MicroFish/frontend/src/views/Home.vue

954 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="home-container">
<!-- Top navigation bar -->
<nav class="navbar">
<div class="nav-brand">MIROFISH</div>
<div class="nav-links">
<LanguageSwitcher />
<a href="https://github.com/666ghj/MiroFish" target="_blank" class="github-link">
{{ $t('nav.visitGithub') }} <span class="arrow"></span>
</a>
</div>
</nav>
<div class="main-content">
<!-- Top half: Hero section -->
<section class="hero-section">
<div class="hero-left">
<div class="tag-row">
<span class="orange-tag">{{ $t('home.tagline') }}</span>
<span class="version-text">{{ $t('home.version') }}</span>
</div>
<h1 class="main-title">
{{ $t('home.heroTitle1') }}<br>
<span class="gradient-text">{{ $t('home.heroTitle2') }}</span>
</h1>
<div class="hero-desc">
<p>
<i18n-t keypath="home.heroDesc" tag="span">
<template #brand><span class="highlight-bold">{{ $t('home.heroDescBrand') }}</span></template>
<template #agentScale><span class="highlight-orange">{{ $t('home.heroDescAgentScale') }}</span></template>
<template #optimalSolution><span class="highlight-code">{{ $t('home.heroDescOptimalSolution') }}</span></template>
</i18n-t>
</p>
<p class="slogan-text">
{{ $t('home.slogan') }}<span class="blinking-cursor">_</span>
</p>
</div>
<div class="decoration-square"></div>
</div>
<div class="hero-right">
<!-- Logo area -->
<div class="logo-container">
<img src="../assets/logo/MiroFish_logo_left.jpeg" alt="MiroFish Logo" class="hero-logo" />
</div>
<button class="scroll-down-btn" @click="scrollToBottom">
</button>
</div>
</section>
<!-- Bottom half: two-column layout -->
<section class="dashboard-section">
<!-- Left column: status & steps -->
<div class="left-panel">
<div class="panel-header">
<span class="status-dot"></span> {{ $t('home.systemStatus') }}
</div>
<h2 class="section-title">{{ $t('home.systemReady') }}</h2>
<p class="section-desc">
{{ $t('home.systemReadyDesc') }}
</p>
<!-- Metrics cards -->
<div class="metrics-row">
<div class="metric-card">
<div class="metric-value">{{ $t('home.metricLowCost') }}</div>
<div class="metric-label">{{ $t('home.metricLowCostDesc') }}</div>
</div>
<div class="metric-card">
<div class="metric-value">{{ $t('home.metricHighAvail') }}</div>
<div class="metric-label">{{ $t('home.metricHighAvailDesc') }}</div>
</div>
</div>
<!-- Project simulation step overview (new section) -->
<div class="steps-container">
<div class="steps-header">
<span class="diamond-icon"></span> {{ $t('home.workflowSequence') }}
</div>
<div class="workflow-list">
<div class="workflow-item">
<span class="step-num">01</span>
<div class="step-info">
<div class="step-title">{{ $t('home.step01Title') }}</div>
<div class="step-desc">{{ $t('home.step01Desc') }}</div>
</div>
</div>
<div class="workflow-item">
<span class="step-num">02</span>
<div class="step-info">
<div class="step-title">{{ $t('home.step02Title') }}</div>
<div class="step-desc">{{ $t('home.step02Desc') }}</div>
</div>
</div>
<div class="workflow-item">
<span class="step-num">03</span>
<div class="step-info">
<div class="step-title">{{ $t('home.step03Title') }}</div>
<div class="step-desc">{{ $t('home.step03Desc') }}</div>
</div>
</div>
<div class="workflow-item">
<span class="step-num">04</span>
<div class="step-info">
<div class="step-title">{{ $t('home.step04Title') }}</div>
<div class="step-desc">{{ $t('home.step04Desc') }}</div>
</div>
</div>
<div class="workflow-item">
<span class="step-num">05</span>
<div class="step-info">
<div class="step-title">{{ $t('home.step05Title') }}</div>
<div class="step-desc">{{ $t('home.step05Desc') }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- Right column: interaction console -->
<div class="right-panel">
<div class="console-box">
<!-- Upload area -->
<div class="console-section">
<div class="console-header">
<span class="console-label">{{ $t('home.realitySeed') }}</span>
<span class="console-meta">{{ $t('home.supportedFormats') }}</span>
</div>
<div
class="upload-zone"
:class="{ 'drag-over': isDragOver, 'has-files': files.length > 0 }"
@dragover.prevent="handleDragOver"
@dragleave.prevent="handleDragLeave"
@drop.prevent="handleDrop"
@click="triggerFileInput"
>
<input
ref="fileInput"
type="file"
multiple
accept=".pdf,.md,.txt"
@change="handleFileSelect"
style="display: none"
:disabled="loading"
/>
<div v-if="files.length === 0" class="upload-placeholder">
<div class="upload-icon">↑</div>
<div class="upload-title">{{ $t('home.dragToUpload') }}</div>
<div class="upload-hint">{{ $t('home.orBrowse') }}</div>
</div>
<div v-else class="file-list">
<div v-for="(file, index) in files" :key="index" class="file-item">
<span class="file-icon">📄</span>
<span class="file-name">{{ file.name }}</span>
<button @click.stop="removeFile(index)" class="remove-btn">×</button>
</div>
</div>
</div>
</div>
<!-- Divider -->
<div class="console-divider">
<span>{{ $t('home.inputParams') }}</span>
</div>
<!-- Input area -->
<div class="console-section">
<div class="console-header">
<span class="console-label">{{ $t('home.simulationPrompt') }}</span>
</div>
<div class="input-wrapper">
<textarea
v-model="formData.simulationRequirement"
class="code-input"
:placeholder="$t('home.promptPlaceholder')"
rows="6"
:disabled="loading"
></textarea>
<div class="model-badge">{{ $t('home.engineBadge') }}</div>
</div>
</div>
<!-- Start button -->
<div class="console-section btn-section">
<button
class="start-engine-btn"
@click="startSimulation"
:disabled="!canSubmit || loading"
>
<span v-if="!loading">{{ $t('home.startEngine') }}</span>
<span v-else>{{ $t('home.initializing') }}</span>
<span class="btn-arrow"></span>
</button>
</div>
</div>
</div>
</section>
<!-- Historical project database -->
<HistoryDatabase />
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import HistoryDatabase from '../components/HistoryDatabase.vue'
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
const router = useRouter()
// Form data
const formData = ref({
simulationRequirement: ''
})
// File list
const files = ref([])
// State
const loading = ref(false)
const error = ref('')
const isDragOver = ref(false)
// File input ref
const fileInput = ref(null)
// Computed: can the form be submitted
const canSubmit = computed(() => {
return formData.value.simulationRequirement.trim() !== '' && files.value.length > 0
})
// Trigger file selection
const triggerFileInput = () => {
if (!loading.value) {
fileInput.value?.click()
}
}
// Handle file selection
const handleFileSelect = (event) => {
const selectedFiles = Array.from(event.target.files)
addFiles(selectedFiles)
}
// Handle drag-and-drop
const handleDragOver = (e) => {
if (!loading.value) {
isDragOver.value = true
}
}
const handleDragLeave = (e) => {
isDragOver.value = false
}
const handleDrop = (e) => {
isDragOver.value = false
if (loading.value) return
const droppedFiles = Array.from(e.dataTransfer.files)
addFiles(droppedFiles)
}
// Add files
const addFiles = (newFiles) => {
const validFiles = newFiles.filter(file => {
const ext = file.name.split('.').pop().toLowerCase()
return ['pdf', 'md', 'txt'].includes(ext)
})
files.value.push(...validFiles)
}
// Remove file
const removeFile = (index) => {
files.value.splice(index, 1)
}
// Scroll to bottom
const scrollToBottom = () => {
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth'
})
}
// Start simulation — navigate immediately; API call happens on Process page
const startSimulation = () => {
if (!canSubmit.value || loading.value) return
// Store the pending upload
import('../store/pendingUpload.js').then(({ setPendingUpload }) => {
setPendingUpload(files.value, formData.value.simulationRequirement)
// Immediately navigate to Process page (with a sentinel projectId for new projects)
router.push({
name: 'Process',
params: { projectId: 'new' }
})
})
}
</script>
<style scoped>
/* Global variables and resets */
:root {
--black: #000000;
--white: #FFFFFF;
--orange: #FF4500;
--gray-light: #F5F5F5;
--gray-text: #666666;
--border: #E5E5E5;
/*
Use Space Grotesk as the primary heading font, JetBrains Mono for code/labels
Make sure these Google Fonts are loaded in index.html
*/
--font-mono: 'JetBrains Mono', monospace;
--font-sans: 'Space Grotesk', 'Noto Sans SC', system-ui, sans-serif;
--font-cn: 'Noto Sans SC', system-ui, sans-serif;
}
.home-container {
min-height: 100vh;
background: var(--white);
font-family: var(--font-sans);
color: var(--black);
}
/* Top navigation */
.navbar {
height: 60px;
background: var(--black);
color: var(--white);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 40px;
}
.nav-brand {
font-family: var(--font-mono);
font-weight: 800;
letter-spacing: 1px;
font-size: 1.2rem;
}
.nav-links {
display: flex;
align-items: center;
gap: 16px;
}
.github-link {
color: var(--white);
text-decoration: none;
font-family: var(--font-mono);
font-size: 0.9rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
transition: opacity 0.2s;
}
.github-link:hover {
opacity: 0.8;
}
.arrow {
font-family: sans-serif;
}
/* Main content area */
.main-content {
max-width: 1400px;
margin: 0 auto;
padding: 60px 40px;
}
/* Hero section */
.hero-section {
display: flex;
justify-content: space-between;
margin-bottom: 80px;
position: relative;
}
.hero-left {
flex: 1;
padding-right: 60px;
}
.tag-row {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 25px;
font-family: var(--font-mono);
font-size: 0.8rem;
}
.orange-tag {
background: var(--orange);
color: var(--white);
padding: 4px 10px;
font-weight: 700;
letter-spacing: 1px;
font-size: 0.75rem;
}
.version-text {
color: #999;
font-weight: 500;
letter-spacing: 0.5px;
}
.main-title {
font-size: 4.5rem;
line-height: 1.2;
font-weight: 500;
margin: 0 0 40px 0;
letter-spacing: -2px;
color: var(--black);
}
.gradient-text {
background: linear-gradient(90deg, #000000 0%, #444444 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
display: inline-block;
}
.hero-desc {
font-size: 1.05rem;
line-height: 1.8;
color: var(--gray-text);
max-width: 640px;
margin-bottom: 50px;
font-weight: 400;
text-align: justify;
}
.hero-desc p {
margin-bottom: 1.5rem;
}
.highlight-bold {
color: var(--black);
font-weight: 700;
}
.highlight-orange {
color: var(--orange);
font-weight: 700;
font-family: var(--font-mono);
}
.highlight-code {
background: rgba(0, 0, 0, 0.05);
padding: 2px 6px;
border-radius: 2px;
font-family: var(--font-mono);
font-size: 0.9em;
color: var(--black);
font-weight: 600;
}
.slogan-text {
font-size: 1.2rem;
font-weight: 520;
color: var(--black);
letter-spacing: 1px;
border-left: 3px solid var(--orange);
padding-left: 15px;
margin-top: 20px;
}
.blinking-cursor {
color: var(--orange);
animation: blink 1s step-end infinite;
font-weight: 700;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.decoration-square {
width: 16px;
height: 16px;
background: var(--orange);
}
.hero-right {
flex: 0.8;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-end;
}
.logo-container {
width: 100%;
display: flex;
justify-content: flex-end;
padding-right: 40px;
}
.hero-logo {
max-width: 500px; /* Adjust logo size */
width: 100%;
}
.scroll-down-btn {
width: 40px;
height: 40px;
border: 1px solid var(--border);
background: transparent;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--orange);
font-size: 1.2rem;
transition: all 0.2s;
}
.scroll-down-btn:hover {
border-color: var(--orange);
}
/* Dashboard two-column layout */
.dashboard-section {
display: flex;
gap: 60px;
border-top: 1px solid var(--border);
padding-top: 60px;
align-items: flex-start;
}
.dashboard-section .left-panel,
.dashboard-section .right-panel {
display: flex;
flex-direction: column;
}
/* Left panel */
.left-panel {
flex: 0.8;
}
.panel-header {
font-family: var(--font-mono);
font-size: 0.8rem;
color: #999;
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 20px;
}
.status-dot {
color: var(--orange);
font-size: 0.8rem;
}
.section-title {
font-size: 2rem;
font-weight: 520;
margin: 0 0 15px 0;
}
.section-desc {
color: var(--gray-text);
margin-bottom: 25px;
line-height: 1.6;
}
.metrics-row {
display: flex;
gap: 20px;
margin-bottom: 15px;
}
.metric-card {
border: 1px solid var(--border);
padding: 20px 30px;
min-width: 150px;
}
.metric-value {
font-family: var(--font-mono);
font-size: 1.8rem;
font-weight: 520;
margin-bottom: 5px;
}
.metric-label {
font-size: 0.85rem;
color: #999;
}
/* Project simulation step overview */
.steps-container {
border: 1px solid var(--border);
padding: 30px;
position: relative;
}
.steps-header {
font-family: var(--font-mono);
font-size: 0.8rem;
color: #999;
margin-bottom: 25px;
display: flex;
align-items: center;
gap: 8px;
}
.diamond-icon {
font-size: 1.2rem;
line-height: 1;
}
.workflow-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.workflow-item {
display: flex;
align-items: flex-start;
gap: 20px;
}
.step-num {
font-family: var(--font-mono);
font-weight: 700;
color: var(--black);
opacity: 0.3;
}
.step-info {
flex: 1;
}
.step-title {
font-weight: 520;
font-size: 1rem;
margin-bottom: 4px;
}
.step-desc {
font-size: 0.85rem;
color: var(--gray-text);
}
/* Right interaction console */
.right-panel {
flex: 1.2;
}
.console-box {
border: 1px solid #CCC; /* Outer solid border */
padding: 8px; /* Inner padding for a double-border effect */
}
.console-section {
padding: 20px;
}
.console-section.btn-section {
padding-top: 0;
}
.console-header {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
font-family: var(--font-mono);
font-size: 0.75rem;
color: #666;
}
.upload-zone {
border: 1px dashed #CCC;
height: 200px;
overflow-y: auto;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
background: #FAFAFA;
}
.upload-zone.has-files {
align-items: flex-start;
}
.upload-zone:hover {
background: #F0F0F0;
border-color: #999;
}
.upload-placeholder {
text-align: center;
}
.upload-icon {
width: 40px;
height: 40px;
border: 1px solid #DDD;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 15px;
color: #999;
}
.upload-title {
font-weight: 500;
font-size: 0.9rem;
margin-bottom: 5px;
}
.upload-hint {
font-family: var(--font-mono);
font-size: 0.75rem;
color: #999;
}
.file-list {
width: 100%;
padding: 15px;
display: flex;
flex-direction: column;
gap: 10px;
}
.file-item {
display: flex;
align-items: center;
background: var(--white);
padding: 8px 12px;
border: 1px solid #EEE;
font-family: var(--font-mono);
font-size: 0.85rem;
}
.file-name {
flex: 1;
margin: 0 10px;
}
.remove-btn {
background: none;
border: none;
cursor: pointer;
font-size: 1.2rem;
color: #999;
}
.console-divider {
display: flex;
align-items: center;
margin: 10px 0;
}
.console-divider::before,
.console-divider::after {
content: '';
flex: 1;
height: 1px;
background: #EEE;
}
.console-divider span {
padding: 0 15px;
font-family: var(--font-mono);
font-size: 0.7rem;
color: #BBB;
letter-spacing: 1px;
}
.input-wrapper {
position: relative;
border: 1px solid #DDD;
background: #FAFAFA;
}
.code-input {
width: 100%;
border: none;
background: transparent;
padding: 20px;
font-family: var(--font-mono);
font-size: 0.9rem;
line-height: 1.6;
resize: vertical;
outline: none;
min-height: 150px;
}
.model-badge {
position: absolute;
bottom: 10px;
right: 15px;
font-family: var(--font-mono);
font-size: 0.7rem;
color: #AAA;
}
.start-engine-btn {
width: 100%;
background: var(--black);
color: var(--white);
border: none;
padding: 20px;
font-family: var(--font-mono);
font-weight: 700;
font-size: 1.1rem;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
letter-spacing: 1px;
position: relative;
overflow: hidden;
}
/* Clickable (non-disabled) state */
.start-engine-btn:not(:disabled) {
background: var(--black);
border: 1px solid var(--black);
animation: pulse-border 2s infinite;
}
.start-engine-btn:hover:not(:disabled) {
background: var(--orange);
border-color: var(--orange);
transform: translateY(-2px);
}
.start-engine-btn:active:not(:disabled) {
transform: translateY(0);
}
.start-engine-btn:disabled {
background: #E5E5E5;
color: #999;
cursor: not-allowed;
transform: none;
border: 1px solid #E5E5E5;
}
/* Onboarding animation: subtle border pulse */
@keyframes pulse-border {
0% { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.2); }
70% { box-shadow: 0 0 0 6px rgba(0, 0, 0, 0); }
100% { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); }
}
/* Responsive */
@media (max-width: 1024px) {
.dashboard-section {
flex-direction: column;
}
.hero-section {
flex-direction: column;
}
.hero-left {
padding-right: 0;
margin-bottom: 40px;
}
.hero-logo {
max-width: 200px;
margin-bottom: 20px;
}
}
</style>
<style>
/* English locale adjustments (unscoped to target html[lang]) */
html[lang="en"] .main-title {
font-size: 3.5rem;
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
letter-spacing: -1px;
}
html[lang="en"] .hero-desc {
text-align: left;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
letter-spacing: 0;
}
html[lang="en"] .slogan-text {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
letter-spacing: 0;
}
html[lang="en"] .tag-row {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
html[lang="en"] .navbar .nav-links {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* Left pane: system status + workflow */
html[lang="en"] .status-section {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
html[lang="en"] .status-section .status-ready {
font-size: 1.6rem;
}
html[lang="en"] .status-section .metric-value {
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 1.4rem;
}
html[lang="en"] .workflow-list .step-title {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
html[lang="en"] .workflow-list .step-desc {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
font-size: 0.72rem !important;
line-height: 1.4 !important;
}
html[lang="en"] .workflow-list {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
</style>