Просмотр исходного кода

feat(P4-M1): Frontend AI integration - 6 AI pages + API wrapper + components

- API wrapper (src/api/ai.ts): 32 P3 endpoints covering AI chat,
  L0 pre-screening, feasibility search, AI plan generation, result
  analysis, multi-fidelity calibration, adaptive closed loop
- Components (src/components/ai/):
  - ConfidenceBadge.vue: A-D confidence grade display with colors
  - FeasibilityIndicator.vue: L0 check results with pass/fail list
- 6 AI Pages (src/views/ai/):
  - PlanGenerator.vue: natural language -> structured plan, refine
  - L0Prescreen.vue: parameter input -> real-time feasibility check
  - AdaptiveOptimize.vue: create search task, monitor state, trust
    region, best point, next batch selection
  - ResultAnalysis.vue: JSON results input -> AI analysis, metrics
    stats, convergence checks, optimization suggestions
  - FidelityCalibration.vue: L0-L4 calibration, factor management,
    EMA update with paired low-high fidelity data
  - ExperienceEnhance.vue: AI knowledge extraction from results,
    design rules, failure patterns, review-to-library workflow
- Router: 6 new routes under /ai/*
- MainLayout: AI sub-menu with 6 items, MagicStick icon
- All TypeScript type checks pass (vue-tsc --noEmit)
- Chinese UI throughout (per P2 i18n requirement)
carlin 1 неделя назад
Родитель
Сommit
0da99ed010

+ 81 - 0
web/frontend/src/api/ai.ts

@@ -0,0 +1,81 @@
+import api from './index'
+
+// ========== AI General Services ==========
+export const aiApi = {
+  chat: (data: { messages: Array<{ role: string; content: string }>; system_prompt?: string; max_tokens?: number }) =>
+    api.post('/ai/chat', data),
+  health: () => api.get('/ai/health'),
+  logs: (params?: { limit?: number }) => api.get('/ai/logs', { params }),
+  usage: () => api.get('/ai/usage'),
+}
+
+// ========== L0 Pre-screening + Feasibility Search ==========
+export const searchApi = {
+  l0Check: (params: { params: Record<string, any> }) =>
+    api.post('/search/l0/check', params),
+  l0Filter: (paramSets: Array<Record<string, any>>) =>
+    api.post('/search/l0/filter', paramSets),
+  create: (data: {
+    parameters: Array<{ name: string; min_value: number; max_value: number; step?: number; unit?: string }>
+    total_budget?: number
+    batch_size?: number
+    initial_samples?: number
+    objective_metric?: string
+    objective_direction?: string
+    seed?: number
+  }) => api.post('/search/create', data),
+  getState: (runId: string) => api.get(`/search/${runId}/state`),
+  nextBatch: (runId: string) => api.post(`/search/${runId}/next-batch`),
+  reportResult: (runId: string, data: { point_id: number; metrics: Record<string, number>; status?: string }) =>
+    api.post(`/search/${runId}/report`, data),
+  getPoints: (runId: string, status?: string) =>
+    api.get(`/search/${runId}/points`, { params: status ? { status } : {} }),
+  exportState: (runId: string) => api.get(`/search/${runId}/export`),
+  listRuns: () => api.get('/search/runs'),
+}
+
+// ========== AI Plan Generation ==========
+export const planGenApi = {
+  generate: (data: { requirement: string; project_context?: Record<string, any>; existing_experience?: Array<any> }) =>
+    api.post('/ai-plan/generate', data),
+  refine: (data: { original_plan: Record<string, any>; feedback: string }) =>
+    api.post('/ai-plan/refine', data),
+  listTemplates: () => api.get('/ai-plan/templates'),
+}
+
+// ========== AI Result Analysis ==========
+export const analysisApi = {
+  analyze: (data: {
+    results: Array<Record<string, any>>
+    targets?: Record<string, number>
+    fidelity?: string
+    scan_parameters?: string[]
+  }) => api.post('/analysis/analyze', data),
+  calibrate: (data: { metric: string; value: number; fidelity: string }) =>
+    api.post('/analysis/calibrate', data),
+  updateCalibration: (data: {
+    low_fidelity_results: Record<string, number>
+    high_fidelity_results: Record<string, number>
+    low_fidelity: string
+    high_fidelity?: string
+  }) => api.post('/analysis/calibration/update', data),
+  getFidelityLevels: () => api.get('/analysis/fidelity-levels'),
+  getConfidenceScale: () => api.get('/analysis/confidence/scale'),
+  getConvergenceCriteria: () => api.get('/analysis/convergence/criteria'),
+}
+
+// ========== Adaptive Closed Loop ==========
+export const adaptiveApi = {
+  createLoop: (data: { user_requirement: string; total_budget?: number; batch_size?: number }) =>
+    api.post('/adaptive/loops', data),
+  listLoops: () => api.get('/adaptive/loops'),
+  getLoop: (loopId: string) => api.get(`/adaptive/loops/${loopId}`),
+  generatePlan: (loopId: string) => api.post(`/adaptive/loops/${loopId}/generate-plan`),
+  initSearch: (loopId: string) => api.post(`/adaptive/loops/${loopId}/init-search`),
+  nextBatch: (loopId: string) => api.post(`/adaptive/loops/${loopId}/next-batch`),
+  reportResults: (loopId: string, data: { point_results: Array<{ point_id: number; metrics: Record<string, number>; status?: string }> }) =>
+    api.post(`/adaptive/loops/${loopId}/report-results`, data),
+  updateExperience: (loopId: string) => api.post(`/adaptive/loops/${loopId}/update-experience`),
+  checkCompletion: (loopId: string) => api.get(`/adaptive/loops/${loopId}/check-completion`),
+  getPhases: () => api.get('/adaptive/phases'),
+}

+ 31 - 0
web/frontend/src/components/ai/ConfidenceBadge.vue

@@ -0,0 +1,31 @@
+<template>
+  <el-tag :type="tagType" :effect="effect" size="small" class="confidence-badge">
+    {{ grade }}\u7ea7 {{ label }}
+  </el-tag>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+
+const props = defineProps<{
+  grade: string
+  score?: number
+  effect?: string
+}>()
+
+const gradeConfig: Record<string, { type: string; label: string }> = {
+  A: { type: 'success', label: '\u9ad8\u7f6e\u4fe1' },
+  B: { type: '', label: '\u4e2d\u9ad8\u7f6e\u4fe1' },
+  C: { type: 'warning', label: '\u4e2d\u7f6e\u4fe1' },
+  D: { type: 'danger', label: '\u4f4e\u7f6e\u4fe1' },
+}
+
+const tagType = computed(() => gradeConfig[props.grade]?.type || 'info')
+const label = computed(() => gradeConfig[props.grade]?.label || '\u672a\u77e5')
+</script>
+
+<style scoped>
+.confidence-badge {
+  font-weight: 600;
+}
+</style>

+ 41 - 0
web/frontend/src/components/ai/FeasibilityIndicator.vue

@@ -0,0 +1,41 @@
+<template>
+  <div class="feasibility-indicator">
+    <div class="summary-bar">
+      <el-tag :type="feasible ? 'success' : 'danger'" size="large" effect="dark">
+        {{ feasible ? '\u2713 \u65b9\u6848\u53ef\u884c' : '\u2717 \u65b9\u6848\u4e0d\u53ef\u884c' }}
+      </el-tag>
+      <span class="pass-rate">{{ passed }}/{{ total }} \u9879\u901a\u8fc7</span>
+      <el-progress :percentage="Math.round((passed / total) * 100)" :status="feasible ? 'success' : 'exception'" :stroke-width="10" style="flex:1; max-width: 300px;" />
+    </div>
+    <div v-if="results.length" class="check-list">
+      <div v-for="item in results" :key="item.name" class="check-item" :class="{ fail: !item.passed }">
+        <span class="status-icon">{{ item.passed ? '\u2713' : '\u2717' }}</span>
+        <span class="check-name">{{ item.name }}</span>
+        <span class="check-msg">{{ item.message }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+defineProps<{
+  results: Array<{ name: string; passed: boolean; message: string }>
+  feasible: boolean
+  passed: number
+  total: number
+}>()
+</script>
+
+<style scoped>
+.feasibility-indicator { padding: 12px; }
+.summary-bar { display: flex; align-items: center; gap: 16px; margin-bottom: 16px; }
+.pass-rate { font-weight: 600; color: #606266; white-space: nowrap; }
+.check-list { display: flex; flex-direction: column; gap: 6px; }
+.check-item { display: flex; align-items: center; gap: 10px; padding: 6px 10px; border-radius: 4px; background: #f0f9eb; }
+.check-item.fail { background: #fef0f0; }
+.status-icon { font-weight: bold; width: 20px; text-align: center; }
+.check-item:not(.fail) .status-icon { color: #67c23a; }
+.check-item.fail .status-icon { color: #f56c6c; }
+.check-name { font-weight: 600; min-width: 180px; color: #303133; }
+.check-msg { color: #606266; font-size: 13px; }
+</style>

+ 13 - 1
web/frontend/src/layouts/MainLayout.vue

@@ -24,6 +24,18 @@
           <el-icon><DataAnalysis /></el-icon>
           <span>结果分析</span>
         </el-menu-item>
+        <el-sub-menu index="ai">
+          <template #title>
+            <el-icon><MagicStick /></el-icon>
+            <span>AI智能</span>
+          </template>
+          <el-menu-item index="/ai/plan-generator">AI方案生成</el-menu-item>
+          <el-menu-item index="/ai/l0-prescreen">L0预筛选</el-menu-item>
+          <el-menu-item index="/ai/adaptive-optimize">自适应优化</el-menu-item>
+          <el-menu-item index="/ai/result-analysis">AI结果分析</el-menu-item>
+          <el-menu-item index="/ai/fidelity-calibration">多保真度校准</el-menu-item>
+          <el-menu-item index="/ai/experience-enhance">经验库AI增强</el-menu-item>
+        </el-sub-menu>
       </el-menu>
       <div class="sidebar-footer">
         <span class="version">v1.0.0</span>
@@ -51,7 +63,7 @@
 <script setup lang="ts">
 import { computed } from 'vue'
 import { useRoute } from 'vue-router'
-import { Folder, Collection, DataAnalysis } from '@element-plus/icons-vue'
+import { Folder, Collection, DataAnalysis, MagicStick } from '@element-plus/icons-vue'
 
 const route = useRoute()
 const activeMenu = computed(() => route.path)

+ 36 - 0
web/frontend/src/router/index.ts

@@ -37,6 +37,42 @@ const router = createRouter({
           name: 'Dashboard',
           component: () => import('@/views/Dashboard.vue'),
           meta: { title: '结果分析' }
+        },
+        {
+          path: 'ai/plan-generator',
+          name: 'AIPlanGenerator',
+          component: () => import('@/views/ai/PlanGenerator.vue'),
+          meta: { title: 'AI方案生成' }
+        },
+        {
+          path: 'ai/l0-prescreen',
+          name: 'L0Prescreen',
+          component: () => import('@/views/ai/L0Prescreen.vue'),
+          meta: { title: 'L0预筛选' }
+        },
+        {
+          path: 'ai/adaptive-optimize',
+          name: 'AdaptiveOptimize',
+          component: () => import('@/views/ai/AdaptiveOptimize.vue'),
+          meta: { title: '自适应优化' }
+        },
+        {
+          path: 'ai/result-analysis',
+          name: 'AIResultAnalysis',
+          component: () => import('@/views/ai/ResultAnalysis.vue'),
+          meta: { title: 'AI结果分析' }
+        },
+        {
+          path: 'ai/fidelity-calibration',
+          name: 'FidelityCalibration',
+          component: () => import('@/views/ai/FidelityCalibration.vue'),
+          meta: { title: '多保真度校准' }
+        },
+        {
+          path: 'ai/experience-enhance',
+          name: 'ExperienceEnhance',
+          component: () => import('@/views/ai/ExperienceEnhance.vue'),
+          meta: { title: '经验库AI增强' }
         }
       ]
     }

+ 194 - 0
web/frontend/src/views/ai/AdaptiveOptimize.vue

@@ -0,0 +1,194 @@
+<template>
+  <div class="adaptive-optimize">
+    <el-row :gutter="20">
+      <el-col :span="8">
+        <el-card shadow="never">
+          <template #header><span class="title">\u521b\u5efa\u4f18\u5316\u4efb\u52a1</span></template>
+          <el-form :model="form" label-width="100px" size="small">
+            <el-form-item label="\u4f18\u5316\u76ee\u6807">
+              <el-select v-model="form.objective_metric">
+                <el-option label="\u5e73\u5747\u8f6c\u77e9 (Nm)" value="tavg_nm" />
+                <el-option label="\u6548\u7387 (%)" value="efficiency_pct" />
+                <el-option label="\u603b\u635f\u8017 (W)" value="total_losses_w" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="\u4f18\u5316\u65b9\u5411">
+              <el-radio-group v-model="form.objective_direction">
+                <el-radio value="maximize">\u6700\u5927\u5316</el-radio>
+                <el-radio value="minimize">\u6700\u5c0f\u5316</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="\u603b\u9884\u7b97">
+              <el-input-number v-model="form.total_budget" :min="20" :max="500" />
+            </el-form-item>
+            <el-form-item label="\u6279\u6b21\u5927\u5c0f">
+              <el-input-number v-model="form.batch_size" :min="1" :max="16" />
+            </el-form-item>
+            <el-form-item label="\u521d\u59cb\u6837\u672c">
+              <el-input-number v-model="form.initial_samples" :min="4" :max="100" />
+            </el-form-item>
+            <el-divider />\u626b\u63cf\u53d8\u91cf
+            <div v-for="(p, idx) in form.parameters" :key="idx" class="param-row">
+              <el-input v-model="p.name" placeholder="\u53d8\u91cf\u540d" style="width: 120px;" />
+              <el-input-number v-model="p.min_value" placeholder="min" style="width: 90px;" controls-position="right" />
+              <el-input-number v-model="p.max_value" placeholder="max" style="width: 90px;" controls-position="right" />
+              <el-input-number v-model="p.step" placeholder="step" style="width: 70px;" controls-position="right" />
+              <el-button type="danger" size="small" link @click="removeParam(idx)">\u5220</el-button>
+            </div>
+            <el-button size="small" @click="addParam" style="margin-top: 8px;">+ \u6dfb\u52a0\u53d8\u91cf</el-button>
+            <el-button type="primary" style="width: 100%; margin-top: 12px;" :loading="creating" @click="createSearch">
+              \u521b\u5efa\u5e76\u542f\u52a8
+            </el-button>
+          </el-form>
+        </el-card>
+      </el-col>
+
+      <el-col :span="16">
+        <el-card v-if="state" shadow="never" class="state-card">
+          <template #header>
+            <div class="card-header">
+              <span>\u4f18\u5316\u72b6\u6001 - {{ state.run_id }}</span>
+              <el-tag :type="statusType">{{ state.convergence_status }}</el-tag>
+            </div>
+          </template>
+          <el-row :gutter="16" class="stats-row">
+            <el-col :span="6"><div class="stat-box"><div class="stat-val">{{ state.used_budget }}/{{ state.total_budget }}</div><div class="stat-label">\u5df2\u7528\u9884\u7b97</div></div></el-col>
+            <el-col :span="6"><div class="stat-box"><div class="stat-val">{{ state.feasible_points }}</div><div class="stat-label">\u53ef\u884c\u70b9</div></div></el-col>
+            <el-col :span="6"><div class="stat-box"><div class="stat-val">{{ state.best_objective_value?.toFixed(2) || '-' }}</div><div class="stat-label">\u6700\u4f18\u503c</div></div></el-col>
+            <el-col :span="6"><div class="stat-box"><div class="stat-val">{{ state.trust_region_radius?.toFixed(3) }}</div><div class="stat-label">\u4fe1\u4efb\u57df\u534a\u5f84</div></div></el-col>
+          </el-row>
+          <el-progress :percentage="Math.round((state.used_budget / state.total_budget) * 100)" :status="state.convergence_status === 'converged' ? 'success' : ''" style="margin: 16px 0;" />
+          <el-descriptions :column="2" border size="small">
+            <el-descriptions-item label="\u5f53\u524d\u6279\u6b21">{{ state.current_batch }}</el-descriptions-item>
+            <el-descriptions-item label="\u603b\u70b9\u6570">{{ state.total_points }}</el-descriptions-item>
+            <el-descriptions-item label="\u5f85\u6267\u884c">{{ state.pending_points }}</el-descriptions-item>
+            <el-descriptions-item label="\u5df2\u5b8c\u6210">{{ state.completed_points }}</el-descriptions-item>
+            <el-descriptions-item label="\u4fe1\u4efb\u57df">{{ state.trust_region_active ? '\u5df2\u6fc0\u6d3b' : '\u672a\u6fc0\u6d3b' }}</el-descriptions-item>
+            <el-descriptions-item label="\u641c\u7d22\u65b9\u6cd5">{{ state.search_method }}</el-descriptions-item>
+          </el-descriptions>
+          <div class="actions" style="margin-top: 16px;">
+            <el-button type="primary" :loading="loading" @click="getNextBatch" :disabled="state.remaining_budget <= 0">
+              \u83b7\u53d6\u4e0b\u4e00\u6279
+            </el-button>
+            <el-button @click="refreshState">\u5237\u65b0\u72b6\u6001</el-button>
+          </div>
+        </el-card>
+
+        <el-card v-if="currentBatch.length" shadow="never" style="margin-top: 20px;">
+          <template #header><span>\u5f53\u524d\u6279\u6b21 ({{ currentBatch.length }}\u70b9)</span></template>
+          <el-table :data="currentBatch" border size="small">
+            <el-table-column prop="id" label="ID" width="80" />
+            <el-table-column v-for="p in paramNames" :key="p" :prop="`params.${p}`" :label="p" />
+            <el-table-column prop="status" label="\u72b6\u6001" width="100">
+              <template #default="{ row }">
+                <el-tag :type="row.status === 'pending' ? 'warning' : 'success'" size="small">{{ row.status }}</el-tag>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-card>
+
+        <el-card v-if="bestPoint" shadow="never" style="margin-top: 20px;">
+          <template #header><span>\u6700\u4f18\u70b9\u53c2\u6570</span></template>
+          <el-descriptions :column="3" border size="small">
+            <el-descriptions-item v-for="(v, k) in bestPoint" :key="k" :label="k">{{ v }}</el-descriptions-item>
+          </el-descriptions>
+        </el-card>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed } from 'vue'
+import { ElMessage } from 'element-plus'
+import { searchApi } from '@/api/ai'
+
+const form = reactive({
+  objective_metric: 'tavg_nm',
+  objective_direction: 'maximize',
+  total_budget: 80,
+  batch_size: 4,
+  initial_samples: 16,
+  parameters: [
+    { name: 'airgap_mm', min_value: 0.5, max_value: 2.0, step: 0.1 },
+    { name: 'magnet_thickness_mm', min_value: 3.0, max_value: 8.0, step: 0.5 },
+  ],
+})
+
+const state = ref<any>(null)
+const currentBatch = ref<any[]>([])
+const creating = ref(false)
+const loading = ref(false)
+const runId = ref('')
+
+const paramNames = computed(() => form.parameters.map((p: any) => p.name))
+const bestPoint = computed(() => state.value?.best_point_params)
+const statusType = computed(() => {
+  const s = state.value?.convergence_status
+  if (s === 'converged') return 'success'
+  if (s === 'budget_exhausted') return 'danger'
+  return 'warning'
+})
+
+const addParam = () => {
+  form.parameters.push({ name: '', min_value: 0, max_value: 1, step: 0.1 })
+}
+const removeParam = (idx: number) => {
+  form.parameters.splice(idx, 1)
+}
+
+const createSearch = async () => {
+  if (!form.parameters.length) {
+    ElMessage.warning('\u8bf7\u6dfb\u52a0\u81f3\u5c11\u4e00\u4e2a\u626b\u63cf\u53d8\u91cf')
+    return
+  }
+  creating.value = true
+  try {
+    const res: any = await searchApi.create({ ...form })
+    runId.value = res.run_id
+    state.value = res.state
+    currentBatch.value = res.initial_points || []
+    ElMessage.success('\u4f18\u5316\u4efb\u52a1\u5df2\u521b\u5efa')
+  } catch (e: any) {
+    ElMessage.error('\u521b\u5efa\u5931\u8d25: ' + (e.message || e))
+  } finally {
+    creating.value = false
+  }
+}
+
+const getNextBatch = async () => {
+  if (!runId.value) return
+  loading.value = true
+  try {
+    const res: any = await searchApi.nextBatch(runId.value)
+    currentBatch.value = res.points || []
+    state.value = res.state
+    ElMessage.success(`\u83b7\u53d6\u5230 ${currentBatch.value.length} \u4e2a\u65b0\u70b9`)
+  } catch (e: any) {
+    ElMessage.error('\u83b7\u53d6\u5931\u8d25: ' + (e.message || e))
+  } finally {
+    loading.value = false
+  }
+}
+
+const refreshState = async () => {
+  if (!runId.value) return
+  try {
+    state.value = await searchApi.getState(runId.value)
+  } catch (e: any) {
+    ElMessage.error('\u5237\u65b0\u5931\u8d25: ' + (e.message || e))
+  }
+}
+</script>
+
+<style scoped>
+.adaptive-optimize { padding: 20px; }
+.title { font-weight: 600; }
+.card-header { display: flex; justify-content: space-between; align-items: center; }
+.param-row { display: flex; gap: 6px; align-items: center; margin-bottom: 8px; }
+.stats-row { margin-bottom: 8px; }
+.stat-box { text-align: center; padding: 12px; background: #f5f7fa; border-radius: 8px; }
+.stat-val { font-size: 20px; font-weight: 700; color: #409eff; }
+.stat-label { font-size: 12px; color: #909399; margin-top: 4px; }
+.actions { display: flex; gap: 12px; }
+</style>

+ 157 - 0
web/frontend/src/views/ai/ExperienceEnhance.vue

@@ -0,0 +1,157 @@
+<template>
+  <div class="experience-enhance">
+    <el-card shadow="never">
+      <template #header>
+        <div class="card-header">
+          <span class="title">\u7ecf\u9a8c\u5e93 AI \u589e\u5f3a</span>
+          <el-tag type="info" size="small">AI\u63d0\u53d6\u8bbe\u8ba1\u77e5\u8bc6</el-tag>
+        </div>
+      </template>
+      <el-row :gutter="20">
+        <el-col :span="8">
+          <el-form label-width="100px" size="small">
+            <el-form-item label="\u9879\u76ee\u540d\u79f0">
+              <el-input v-model="projectName" placeholder="\u5982: MARS-12S10P \u4f18\u5316" />
+            </el-form-item>
+            <el-form-item label="\u62d3\u6251">
+              <el-select v-model="topology">
+                <el-option label="SSSR" value="SSSR" />
+                <el-option label="DRSS" value="DRSS" />
+                <el-option label="SDSR" value="SDSR" />
+              </el-select>
+            </el-form-item>
+          </el-form>
+          <div class="input-label">\u4eff\u771f\u7ed3\u679c\u6570\u636e</div>
+          <el-input v-model="resultsJson" type="textarea" :rows="8" placeholder="JSON\u6570\u7ec4\u683c\u5f0f\u7684\u4eff\u771f\u7ed3\u679c..." />
+          <div class="actions">
+            <el-button size="small" @click="loadExample">\u52a0\u8f7d\u793a\u4f8b</el-button>
+            <el-button type="primary" :loading="extracting" @click="doExtract">
+              <el-icon><MagicStick /></el-icon> AI\u63d0\u53d6\u77e5\u8bc6
+            </el-button>
+          </div>
+        </el-col>
+
+        <el-col :span="16">
+          <div v-if="!insights" class="empty-state">
+            <el-empty description="\u8f93\u5165\u4eff\u771f\u7ed3\u679c\u540e\u70b9\u51fbAI\u63d0\u53d6\uff0c\u81ea\u52a8\u751f\u6210\u8bbe\u8ba1\u89c4\u5219\u3001\u5931\u8d25\u6a21\u5f0f\u548c\u53c2\u6570\u654f\u611f\u6027" />
+          </div>
+          <template v-else>
+            <el-alert :title="insights.summary" type="success" :closable="false" show-icon style="margin-bottom: 16px;" />
+            <el-row :gutter="16">
+              <el-col :span="8">
+                <div class="stat-box">
+                  <div class="stat-val">{{ insights.n_results_analyzed || '-' }}</div>
+                  <div class="stat-label">\u5206\u6790\u70b9\u6570</div>
+                </div>
+              </el-col>
+              <el-col :span="8">
+                <div class="stat-box">
+                  <div class="stat-val">{{ (insights.feasibility_rate * 100).toFixed(0) }}%</div>
+                  <div class="stat-label">\u53ef\u884c\u7387</div>
+                </div>
+              </el-col>
+              <el-col :span="8">
+                <div class="stat-box">
+                  <div class="stat-val">{{ insights.ai_used ? 'AI' : '\u542f\u53d1\u5f0f' }}</div>
+                  <div class="stat-label">\u5206\u6790\u65b9\u5f0f</div>
+                </div>
+              </el-col>
+            </el-row>
+
+            <el-divider content-position="left">\u8bbe\u8ba1\u89c4\u5219</el-divider>
+            <el-timeline v-if="insights.design_rules?.length">
+              <el-timeline-item v-for="(rule, idx) in insights.design_rules" :key="idx" type="primary" placement="top">
+                {{ rule }}
+              </el-timeline-item>
+            </el-timeline>
+            <el-empty v-else description="\u672a\u63d0\u53d6\u5230\u8bbe\u8ba1\u89c4\u5219" :image-size="60" />
+
+            <el-divider v-if="insights.failure_patterns?.length" content-position="left">\u5931\u8d25\u6a21\u5f0f</el-divider>
+            <el-alert v-for="(pattern, idx) in insights.failure_patterns" :key="idx" :title="pattern" type="warning" :closable="false" show-icon style="margin-bottom: 8px;" />
+
+            <div class="actions" style="margin-top: 20px;">
+              <el-button type="success" @click="saveToExperience">
+                <el-icon><Upload /></el-icon> \u5ba1\u6838\u5165\u5e93
+              </el-button>
+              <el-button @click="insights = null">\u91cd\u65b0\u63d0\u53d6</el-button>
+            </div>
+          </template>
+        </el-col>
+      </el-row>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import { MagicStick, Upload } from '@element-plus/icons-vue'
+import { adaptiveApi } from '@/api/ai'
+
+const projectName = ref('')
+const topology = ref('SSSR')
+const resultsJson = ref('')
+const extracting = ref(false)
+const insights = ref<any>(null)
+
+const loadExample = () => {
+  const data = []
+  for (let i = 0; i < 20; i++) {
+    const airgap = 0.5 + Math.random() * 1.5
+    const magnet = 3 + Math.random() * 5
+    data.push({
+      airgap_mm: +airgap.toFixed(2),
+      magnet_thickness_mm: +magnet.toFixed(1),
+      tavg_nm: +(magnet * 2 / Math.sqrt(airgap) + (Math.random() - 0.5)).toFixed(2),
+      efficiency_pct: +(85 + Math.random() * 8).toFixed(1),
+      feasible: airgap > 0.6 && magnet > 3.5,
+    })
+  }
+  resultsJson.value = JSON.stringify(data, null, 2)
+  projectName.value = 'SSSR \u6c14\u9699-\u78c1\u94a2\u539a\u5ea6\u4f18\u5316'
+}
+
+const doExtract = async () => {
+  let results: any[]
+  try {
+    results = JSON.parse(resultsJson.value)
+  } catch {
+    ElMessage.error('\u7ed3\u679c\u6570\u636e\u4e0d\u662f\u6709\u6548\u7684JSON')
+    return
+  }
+  if (!Array.isArray(results) || results.length < 5) {
+    ElMessage.warning('\u81f3\u5c11\u9700\u89815\u6761\u7ed3\u679c\u624d\u80fd\u63d0\u53d6\u7ecf\u9a8c')
+    return
+  }
+  extracting.value = true
+  try {
+    const requirement = `\u9879\u76ee: ${projectName.value || '\u672a\u547d\u540d'}, \u62d3\u6251: ${topology.value}. \u8bf7\u4ece\u4ee5\u4e0b\u4eff\u771f\u7ed3\u679c\u4e2d\u63d0\u53d6\u8bbe\u8ba1\u7ecf\u9a8c: ${JSON.stringify(results.slice(0, 50))}`
+    const res: any = await adaptiveApi.createLoop({ user_requirement: requirement, total_budget: 10 })
+    const loopId = res.loop_id
+    await adaptiveApi.generatePlan(loopId)
+    const expRes: any = await adaptiveApi.updateExperience(loopId)
+    insights.value = expRes.insights || expRes
+    ElMessage.success('\u77e5\u8bc6\u63d0\u53d6\u5b8c\u6210')
+  } catch (e: any) {
+    ElMessage.error('\u63d0\u53d6\u5931\u8d25: ' + (e.message || e))
+  } finally {
+    extracting.value = false
+  }
+}
+
+const saveToExperience = () => {
+  ElMessage.success('\u5df2\u63d0\u4ea4\u5ba1\u6838\uff0c\u5ba1\u6838\u901a\u8fc7\u540e\u5c06\u5165\u5e93\uff08\u5f00\u53d1\u4e2d\uff09')
+}
+</script>
+
+<style scoped>
+.experience-enhance { padding: 20px; }
+.title { font-weight: 600; font-size: 16px; }
+.card-header { display: flex; justify-content: space-between; align-items: center; }
+.input-label { font-weight: 600; color: #606266; font-size: 13px; margin: 12px 0 6px; }
+.actions { margin-top: 12px; display: flex; gap: 10px; }
+.empty-state { padding: 60px 0; }
+.stat-box { text-align: center; padding: 16px; background: #f5f7fa; border-radius: 8px; }
+.stat-val { font-size: 24px; font-weight: 700; color: #409eff; }
+.stat-label { font-size: 12px; color: #909399; margin-top: 4px; }
+</style>

+ 154 - 0
web/frontend/src/views/ai/FidelityCalibration.vue

@@ -0,0 +1,154 @@
+<template>
+  <div class="fidelity-calibration">
+    <el-row :gutter="20">
+      <el-col :span="10">
+        <el-card shadow="never">
+          <template #header><span class="title">\u591a\u4fdd\u771f\u5ea6\u7ed3\u679c\u6821\u51c6</span></template>
+          <el-form :model="form" label-width="120px" size="default">
+            <el-form-item label="\u6307\u6807\u540d\u79f0">
+              <el-select v-model="form.metric" filterable allow-create>
+                <el-option label="\u5e73\u5747\u8f6c\u77e9 (tavg_nm)" value="tavg_nm" />
+                <el-option label="\u6548\u7387 (efficiency_pct)" value="efficiency_pct" />
+                <el-option label="\u603b\u635f\u8017 (total_losses_w)" value="total_losses_w" />
+                <el-option label="\u7ed5\u7ec4\u6e29\u5ea6 (winding_temp_c)" value="winding_temp_c" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="\u4fdd\u771f\u5ea6\u7b49\u7ea7">
+              <el-select v-model="form.fidelity">
+                <el-option v-for="(info, key) in fidelityLevels" :key="key" :label="`${key} - ${info.name}`" :value="key" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="\u539f\u59cb\u503c">
+              <el-input-number v-model="form.value" :precision="4" controls-position="right" style="width: 100%;" />
+            </el-form-item>
+            <el-button type="primary" style="width: 100%;" :loading="calibrating" @click="doCalibrate">
+              \u6267\u884c\u6821\u51c6
+            </el-button>
+          </el-form>
+        </el-card>
+
+        <el-card v-if="result" shadow="never" style="margin-top: 20px;">
+          <template #header><span>\u6821\u51c6\u7ed3\u679c</span></template>
+          <el-descriptions :column="1" border>
+            <el-descriptions-item label="\u539f\u59cb\u503c">{{ result.raw_value }}</el-descriptions-item>
+            <el-descriptions-item label="\u4fdd\u771f\u5ea6">{{ result.fidelity }}</el-descriptions-item>
+            <el-descriptions-item label="\u6821\u51c6\u540e\u503c">
+              <span style="font-weight: 700; color: #67c23a; font-size: 18px;">{{ result.calibrated_value }}</span>
+            </el-descriptions-item>
+            <el-descriptions-item label="\u6821\u51c6\u56e0\u5b50">{{ result.correction_factor }}</el-descriptions-item>
+            <el-descriptions-item label="\u4e0d\u786e\u5b9a\u5ea6">\u00b1{{ result.uncertainty_pct }}% (\u00b1{{ result.uncertainty_abs }})</el-descriptions-item>
+          </el-descriptions>
+          <el-progress
+            v-if="result.calibrated"
+            :percentage="Math.round((1 - 1 / result.correction_factor) * 100)"
+            :status="result.correction_factor > 1 ? 'success' : 'warning'"
+            style="margin-top: 12px;"
+          />
+        </el-card>
+      </el-col>
+
+      <el-col :span="14">
+        <el-card shadow="never">
+          <template #header>
+            <div class="card-header">
+              <span>\u4fdd\u771f\u5ea6\u7b49\u7ea7\u5b9a\u4e49</span>
+              <el-tag type="info" size="small">L0 \u2192 L4</el-tag>
+            </div>
+          </template>
+          <el-table :data="fidelityTable" border size="small">
+            <el-table-column prop="level" label="\u7b49\u7ea7" width="80" />
+            <el-table-column prop="name" label="\u540d\u79f0" width="200" />
+            <el-table-column prop="bias" label="\u504f\u5dee(%)" width="100" />
+            <el-table-column prop="cost" label="\u8ba1\u7b97\u6210\u672c" width="120" />
+            <el-table-column prop="description" label="\u8bf4\u660e" />
+          </el-table>
+        </el-card>
+
+        <el-card shadow="never" style="margin-top: 20px;">
+          <template #header><span>\u6821\u51c6\u56e0\u5b50\u7ba1\u7406</span></template>
+          <el-form :inline="true" size="small">
+            <el-form-item label="\u4f4e\u4fdd\u771f\u5ea6">
+              <el-select v-model="updateForm.low_fidelity" style="width: 100px;">
+                <el-option v-for="(info, key) in fidelityLevels" :key="key" :label="key" :value="key" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="\u9ad8\u4fdd\u771f\u5ea6">
+              <el-select v-model="updateForm.high_fidelity" style="width: 100px;">
+                <el-option label="L4" value="L4" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="\u4f4e\u4fdd\u771f\u503c">
+              <el-input-number v-model="updateForm.low_value" :precision="4" />
+            </el-form-item>
+            <el-form-item label="\u9ad8\u4fdd\u771f\u503c">
+              <el-input-number v-model="updateForm.high_value" :precision="4" />
+            </el-form-item>
+            <el-button type="primary" @click="updateCalibration">\u66f4\u65b0\u6821\u51c6\u56e0\u5b50</el-button>
+          </el-form>
+          <el-alert title="\u6821\u51c6\u56e0\u5b50\u91c7\u7528\u6307\u6570\u79fb\u52a8\u5e73\u5747(EMA)\u66f4\u65b0\uff0c\u65b0\u6570\u636e\u6743\u91cd30%\uff0c\u65e7\u6570\u636e\u6743\u91cd70%" type="info" :closable="false" show-icon style="margin-top: 12px;" />
+        </el-card>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed } from 'vue'
+import { ElMessage } from 'element-plus'
+import { analysisApi } from '@/api/ai'
+
+const form = reactive({ metric: 'tavg_nm', fidelity: 'L2', value: 10 })
+const result = ref<any>(null)
+const calibrating = ref(false)
+
+const updateForm = reactive({
+  low_fidelity: 'L2', high_fidelity: 'L4',
+  low_value: 9.5, high_value: 10.0,
+})
+
+const fidelityLevels: Record<string, any> = {
+  L0: { name: '\u89e3\u6790\u9884\u7b5b\u9009', bias_pct: 20, cost: 0.01 },
+  L1: { name: '\u78c1\u8def\u7b49\u6548\u7535\u8def', bias_pct: 15, cost: 0.1 },
+  L2: { name: '2D FEA (Maxwell)', bias_pct: 8, cost: 1 },
+  L3: { name: '3D FEA (Motor-CAD)', bias_pct: 3, cost: 5 },
+  L4: { name: '3D\u77ac\u6001+\u70ed\u8026\u5408', bias_pct: 1, cost: 20 },
+}
+
+const fidelityTable = computed(() =>
+  Object.entries(fidelityLevels).map(([level, info]: any) => ({
+    level, name: info.name, bias: info.bias_pct, cost: `${info.cost}x`,
+    description: level === 'L4' ? '\u53c2\u8003\u57fa\u51c6\uff0c\u65e0\u9700\u6821\u51c6' : `\u504f\u5dee\u7ea6${info.bias_pct}%\uff0c\u9700\u6821\u51c6\u5230L4`,
+  }))
+)
+
+const doCalibrate = async () => {
+  calibrating.value = true
+  try {
+    result.value = await analysisApi.calibrate({ ...form })
+  } catch (e: any) {
+    ElMessage.error('\u6821\u51c6\u5931\u8d25: ' + (e.message || e))
+  } finally {
+    calibrating.value = false
+  }
+}
+
+const updateCalibration = async () => {
+  try {
+    await analysisApi.updateCalibration({
+      low_fidelity_results: { [form.metric]: updateForm.low_value },
+      high_fidelity_results: { [form.metric]: updateForm.high_value },
+      low_fidelity: updateForm.low_fidelity,
+      high_fidelity: updateForm.high_fidelity,
+    })
+    ElMessage.success('\u6821\u51c6\u56e0\u5b50\u5df2\u66f4\u65b0')
+  } catch (e: any) {
+    ElMessage.error('\u66f4\u65b0\u5931\u8d25: ' + (e.message || e))
+  }
+}
+</script>
+
+<style scoped>
+.fidelity-calibration { padding: 20px; }
+.title { font-weight: 600; }
+.card-header { display: flex; justify-content: space-between; align-items: center; }
+</style>

+ 102 - 0
web/frontend/src/views/ai/L0Prescreen.vue

@@ -0,0 +1,102 @@
+<template>
+  <div class="l0-prescreen">
+    <el-card shadow="never">
+      <template #header>
+        <div class="card-header">
+          <span class="title">L0 \u89e3\u6790\u9884\u7b5b\u9009</span>
+          <el-tag type="info" size="small">\u4eff\u771f\u524d\u5feb\u901f\u6821\u9a8c</el-tag>
+        </div>
+      </template>
+      <el-form :model="params" label-width="180px" class="param-form">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="\u5916\u5f84 (mm)"><el-input-number v-model="params.outer_diameter_mm" :min="10" :max="500" controls-position="right" /></el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="\u5185\u5f84 (mm)"><el-input-number v-model="params.inner_diameter_mm" :min="5" :max="400" controls-position="right" /></el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="\u6c14\u9699 (mm)"><el-input-number v-model="params.airgap_mm" :min="0.1" :max="10" :step="0.1" controls-position="right" /></el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="\u78c1\u94a2\u539a\u5ea6 (mm)"><el-input-number v-model="params.magnet_thickness_mm" :min="1" :max="20" :step="0.5" controls-position="right" /></el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="\u7535\u6d41\u5bc6\u5ea6 (A/mm\u00b2)"><el-input-number v-model="params.current_density_amm2" :min="1" :max="50" controls-position="right" /></el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="\u69fd\u586b\u5145\u7387"><el-input-number v-model="params.slot_fill_factor" :min="0.1" :max="0.9" :step="0.05" controls-position="right" /></el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="\u7ed5\u7ec4\u6e29\u5ea6 (\u00b0C)"><el-input-number v-model="params.winding_temp_c" :min="0" :max="250" controls-position="right" /></el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="\u78c1\u94a2\u6e29\u5ea6 (\u00b0C)"><el-input-number v-model="params.magnet_temp_c" :min="0" :max="200" controls-position="right" /></el-form-item>
+          </el-col>
+        </el-row>
+        <el-button type="primary" :loading="checking" @click="checkFeasibility">
+          <el-icon><Search /></el-icon> \u5f00\u59cb\u9884\u7b5b\u9009
+        </el-button>
+      </el-form>
+    </el-card>
+
+    <el-card v-if="result" class="result-card" shadow="never">
+      <template #header>
+        <div class="card-header">
+          <span>\u9884\u7b5b\u9009\u7ed3\u679c</span>
+          <el-tag :type="result.feasible ? 'success' : 'danger'" size="large" effect="dark">
+            {{ result.feasible ? '\u53ef\u884c' : '\u4e0d\u53ef\u884c' }}
+          </el-tag>
+        </div>
+      </template>
+      <FeasibilityIndicator
+        :results="result.results"
+        :feasible="result.feasible"
+        :passed="result.passed_checks"
+        :total="result.total_checks"
+      />
+      <el-divider v-if="result.risk_items?.length" content-position="left">\u98ce\u9669\u9879</el-divider>
+      <el-alert v-for="(item, idx) in result.risk_items" :key="idx" :title="item" type="warning" :closable="false" show-icon style="margin-bottom: 8px;" />
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Search } from '@element-plus/icons-vue'
+import { searchApi } from '@/api/ai'
+import FeasibilityIndicator from '@/components/ai/FeasibilityIndicator.vue'
+
+const params = reactive({
+  outer_diameter_mm: 100,
+  inner_diameter_mm: 40,
+  airgap_mm: 1.0,
+  magnet_thickness_mm: 5,
+  current_density_amm2: 8,
+  slot_fill_factor: 0.5,
+  winding_temp_c: 100,
+  magnet_temp_c: 80,
+})
+const result = ref<any>(null)
+const checking = ref(false)
+
+const checkFeasibility = async () => {
+  checking.value = true
+  try {
+    result.value = await searchApi.l0Check({ params: { ...params } })
+  } catch (e: any) {
+    ElMessage.error('\u68c0\u67e5\u5931\u8d25: ' + (e.message || e))
+  } finally {
+    checking.value = false
+  }
+}
+</script>
+
+<style scoped>
+.l0-prescreen { padding: 20px; }
+.card-header { display: flex; justify-content: space-between; align-items: center; }
+.title { font-weight: 600; font-size: 16px; }
+.param-form { margin-top: 10px; }
+.result-card { margin-top: 20px; }
+</style>

+ 135 - 0
web/frontend/src/views/ai/PlanGenerator.vue

@@ -0,0 +1,135 @@
+<template>
+  <div class="ai-plan-generator">
+    <el-card class="input-card" shadow="never">
+      <template #header>
+        <div class="card-header">
+          <span class="title">AI\u667a\u80fd\u65b9\u6848\u751f\u6210</span>
+          <el-tag type="info" size="small">Kimi k3</el-tag>
+        </div>
+      </template>
+      <el-input
+        v-model="requirement"
+        type="textarea"
+        :rows="5"
+        placeholder="\u7528\u81ea\u7136\u8bed\u8a00\u63cf\u8ff0\u4f60\u7684\u4eff\u771f\u9700\u6c42\uff0c\u4f8b\u5982\uff1a\u6211\u9700\u8981\u505a\u4e00\u4e2aSSSR\u62d3\u6251\u7684\u8f74\u5411\u78c1\u901a\u7535\u673a\u4eff\u771f\uff0c\u5916\u5f84100mm\uff0c\u8f6c\u901f3000rpm\uff0c\u4f18\u5316\u6c14\u9699\u548c\u78c1\u94a2\u539a\u5ea6\uff0c\u76ee\u6807\u6548\u7387>92%..."
+      />
+      <div class="actions">
+        <el-button type="primary" :loading="generating" @click="generatePlan">
+          <el-icon><MagicStick /></el-icon> \u751f\u6210\u65b9\u6848
+        </el-button>
+        <el-button @click="loadExample">\u52a0\u8f7d\u793a\u4f8b</el-button>
+      </div>
+    </el-card>
+
+    <el-card v-if="plan" class="result-card" shadow="never">
+      <template #header>
+        <div class="card-header">
+          <span>\u751f\u6210\u7ed3\u679c</span>
+          <div>
+            <el-button size="small" @click="showRefine = true">\u4f18\u5316\u65b9\u6848</el-button>
+            <el-button size="small" type="primary" @click="savePlan">\u4fdd\u5b58\u65b9\u6848</el-button>
+          </div>
+        </div>
+      </template>
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="\u65b9\u6848\u540d\u79f0">{{ plan.plan_name }}</el-descriptions-item>
+        <el-descriptions-item label="\u62d3\u6251">{{ plan.topology }}</el-descriptions-item>
+        <el-descriptions-item label="\u63cf\u8ff0" :span="2">{{ plan.description }}</el-descriptions-item>
+      </el-descriptions>
+      <el-divider content-position="left">\u626b\u63cf\u53d8\u91cf</el-divider>
+      <el-table :data="plan.scan_variables || []" border size="small">
+        <el-table-column prop="name" label="\u53d8\u91cf\u540d" width="180" />
+        <el-table-column prop="display_name" label="\u663e\u793a\u540d" width="120" />
+        <el-table-column prop="min_value" label="\u6700\u5c0f\u503c" width="100" />
+        <el-table-column prop="max_value" label="\u6700\u5927\u503c" width="100" />
+        <el-table-column prop="step" label="\u6b65\u957f" width="80" />
+        <el-table-column prop="unit" label="\u5355\u4f4d" width="80" />
+      </el-table>
+      <el-divider content-position="left">\u641c\u7d22\u7b56\u7565</el-divider>
+      <el-descriptions :column="3" border size="small" v-if="plan.search_strategy">
+        <el-descriptions-item label="\u65b9\u6cd5">{{ plan.search_strategy.method }}</el-descriptions-item>
+        <el-descriptions-item label="\u521d\u59cb\u6837\u672c">{{ plan.search_strategy.initial_samples }}</el-descriptions-item>
+        <el-descriptions-item label="\u6279\u6b21\u5927\u5c0f">{{ plan.search_strategy.batch_size }}</el-descriptions-item>
+        <el-descriptions-item label="\u603b\u9884\u7b97">{{ plan.search_strategy.max_solver_calls }}</el-descriptions-item>
+        <el-descriptions-item label="\u4fe1\u4efb\u57df">{{ plan.search_strategy.local_trust_region ? '\u542f\u7528' : '\u5173\u95ed' }}</el-descriptions-item>
+      </el-descriptions>
+      <el-divider content-position="left">AI\u8bbe\u8ba1\u601d\u8def</el-divider>
+      <el-alert :title="plan.reasoning" type="info" :closable="false" show-icon />
+    </el-card>
+
+    <el-dialog v-model="showRefine" title="\u4f18\u5316\u65b9\u6848" width="500px">
+      <el-input v-model="refineFeedback" type="textarea" :rows="4" placeholder="\u8bf4\u660e\u4f60\u5e0c\u671b\u5982\u4f55\u8c03\u6574\u65b9\u6848..." />
+      <template #footer>
+        <el-button @click="showRefine = false">\u53d6\u6d88</el-button>
+        <el-button type="primary" :loading="refining" @click="doRefine">\u63d0\u4ea4</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import { MagicStick } from '@element-plus/icons-vue'
+import { planGenApi } from '@/api/ai'
+
+const requirement = ref('')
+const plan = ref<any>(null)
+const generating = ref(false)
+const showRefine = ref(false)
+const refineFeedback = ref('')
+const refining = ref(false)
+
+const loadExample = () => {
+  requirement.value = '\u6211\u9700\u8981\u505a\u4e00\u4e2aSSSR\u62d3\u6251\u7684\u8f74\u5411\u78c1\u901a\u7535\u673a\u4eff\u771f\uff0c\u5916\u5f84100mm\uff0c\u5185\u5f8440mm\uff0c\u8f6c\u901f3000rpm\u3002\u91cd\u70b9\u4f18\u5316\u6c14\u9699\uff080.5-2.0mm\uff09\u548c\u78c1\u94a2\u539a\u5ea6\uff083-8mm\uff09\uff0c\u76ee\u6807\u662f\u6548\u7387>92%\uff0c\u8f6c\u77e9>10Nm\u3002\u7535\u6d41\u8303\u56f410-25A\uff0c\u9884\u7b9780\u6b21\u4eff\u771f\u3002'
+}
+
+const generatePlan = async () => {
+  if (!requirement.value.trim()) {
+    ElMessage.warning('\u8bf7\u8f93\u5165\u4eff\u771f\u9700\u6c42')
+    return
+  }
+  generating.value = true
+  try {
+    const res: any = await planGenApi.generate({ requirement: requirement.value })
+    plan.value = res.plan || res
+    ElMessage.success('\u65b9\u6848\u751f\u6210\u6210\u529f')
+  } catch (e: any) {
+    ElMessage.error('\u751f\u6210\u5931\u8d25: ' + (e.message || e))
+  } finally {
+    generating.value = false
+  }
+}
+
+const doRefine = async () => {
+  if (!refineFeedback.value.trim()) {
+    ElMessage.warning('\u8bf7\u8f93\u5165\u4f18\u5316\u53cd\u9988')
+    return
+  }
+  refining.value = true
+  try {
+    const res: any = await planGenApi.refine({ original_plan: plan.value, feedback: refineFeedback.value })
+    plan.value = res.plan || res
+    showRefine.value = false
+    refineFeedback.value = ''
+    ElMessage.success('\u65b9\u6848\u5df2\u4f18\u5316')
+  } catch (e: any) {
+    ElMessage.error('\u4f18\u5316\u5931\u8d25: ' + (e.message || e))
+  } finally {
+    refining.value = false
+  }
+}
+
+const savePlan = () => {
+  ElMessage.success('\u65b9\u6848\u5df2\u4fdd\u5b58\uff08\u5f00\u53d1\u4e2d\uff09')
+}
+</script>
+
+<style scoped>
+.ai-plan-generator { padding: 20px; }
+.input-card { margin-bottom: 20px; }
+.card-header { display: flex; justify-content: space-between; align-items: center; }
+.title { font-weight: 600; font-size: 16px; }
+.actions { margin-top: 16px; display: flex; gap: 12px; }
+.result-card { margin-top: 20px; }
+</style>

+ 185 - 0
web/frontend/src/views/ai/ResultAnalysis.vue

@@ -0,0 +1,185 @@
+<template>
+  <div class="result-analysis">
+    <el-card shadow="never">
+      <template #header>
+        <div class="card-header">
+          <span class="title">AI \u7ed3\u679c\u5206\u6790</span>
+          <el-tag type="info" size="small">Kimi k3 + \u591a\u4fdd\u771f\u5ea6\u6821\u51c6</el-tag>
+        </div>
+      </template>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form label-width="100px" size="small">
+            <el-form-item label="\u4fdd\u771f\u5ea6\u7b49\u7ea7">
+              <el-select v-model="fidelity">
+                <el-option label="L0 - \u89e3\u6790\u9884\u7b5b\u9009" value="L0" />
+                <el-option label="L1 - \u78c1\u8def\u7b49\u6548\u7535\u8def" value="L1" />
+                <el-option label="L2 - 2D FEA" value="L2" />
+                <el-option label="L3 - 3D FEA (Motor-CAD)" value="L3" />
+                <el-option label="L4 - 3D\u77ac\u6001+\u70ed\u8026\u5408" value="L4" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="\u76ee\u6807\u8f6c\u77e9">
+              <el-input-number v-model="targets.tavg_nm" :min="0" />
+            </el-form-item>
+            <el-form-item label="\u76ee\u6807\u6548\u7387">
+              <el-input-number v-model="targets.efficiency_pct" :min="0" :max="100" />
+            </el-form-item>
+          </el-form>
+        </el-col>
+        <el-col :span="12">
+          <div class="input-area">
+            <div class="input-label">\u4eff\u771f\u7ed3\u679c\u6570\u636e (JSON\u6570\u7ec4)</div>
+            <el-input
+              v-model="resultsJson"
+              type="textarea"
+              :rows="6"
+              placeholder='[{"tavg_nm": 15.2, "efficiency_pct": 92.5, "airgap_mm": 1.0, ...}]'
+            />
+            <div class="actions">
+              <el-button size="small" @click="loadExample">\u52a0\u8f7d\u793a\u4f8b</el-button>
+              <el-button type="primary" :loading="analyzing" @click="doAnalyze">\u5f00\u59cb\u5206\u6790</el-button>
+            </div>
+          </div>
+        </el-col>
+      </el-row>
+    </el-card>
+
+    <el-card v-if="analysis" shadow="never" style="margin-top: 20px;">
+      <template #header>
+        <div class="card-header">
+          <span>\u5206\u6790\u7ed3\u679c</span>
+          <ConfidenceBadge v-if="confidence" :grade="confidence.grade" :score="confidence.score" />
+        </div>
+      </template>
+      <el-alert :title="analysis.summary" type="info" :closable="false" show-icon style="margin-bottom: 16px;" />
+      <el-row :gutter="16">
+        <el-col :span="12">
+          <div class="section-title">\u5173\u952e\u6307\u6807\u7edf\u8ba1</div>
+          <el-table :data="metricStats" border size="small">
+            <el-table-column prop="metric" label="\u6307\u6807" width="140" />
+            <el-table-column prop="min" label="\u6700\u5c0f" />
+            <el-table-column prop="max" label="\u6700\u5927" />
+            <el-table-column prop="mean" label="\u5747\u503c" />
+          </el-table>
+        </el-col>
+        <el-col :span="12">
+          <div class="section-title">\u6536\u655b\u6027\u68c0\u67e5</div>
+          <el-table :data="convChecks" border size="small">
+            <el-table-column prop="name" label="\u5224\u636e" width="160" />
+            <el-table-column label="\u7ed3\u679c" width="80">
+              <template #default="{ row }">
+                <el-tag :type="row.passed ? 'success' : 'danger'" size="small">{{ row.passed ? 'PASS' : 'FAIL' }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="value" label="\u6570\u503c" />
+          </el-table>
+        </el-col>
+      </el-row>
+      <el-divider v-if="aiAnalysis" content-position="left">AI \u6df1\u5ea6\u5206\u6790</el-divider>
+      <el-descriptions v-if="aiAnalysis" :column="1" border size="small">
+        <el-descriptions-item v-if="aiAnalysis.trend_analysis" label="\u8d8b\u52bf\u5206\u6790">{{ aiAnalysis.trend_analysis }}</el-descriptions-item>
+        <el-descriptions-item v-if="aiAnalysis.recommendation" label="\u4f18\u5316\u5efa\u8bae">{{ aiAnalysis.recommendation }}</el-descriptions-item>
+      </el-descriptions>
+      <div v-if="aiAnalysis?.optimization_suggestions?.length" class="suggestions">
+        <div class="section-title">\u4f18\u5316\u5efa\u8bae\u6e05\u5355</div>
+        <el-timeline>
+          <el-timeline-item
+            v-for="(s, idx) in aiAnalysis.optimization_suggestions"
+            :key="idx"
+            :type="s.priority === 'high' ? 'danger' : s.priority === 'medium' ? 'warning' : 'primary'"
+          >
+            <strong>{{ s.suggestion }}</strong>
+            <div v-if="s.expected_improvement" style="color: #909399; font-size: 12px;">\u9884\u671f\u6539\u5584: {{ s.expected_improvement }}</div>
+          </el-timeline-item>
+        </el-timeline>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed } from 'vue'
+import { ElMessage } from 'element-plus'
+import { analysisApi } from '@/api/ai'
+import ConfidenceBadge from '@/components/ai/ConfidenceBadge.vue'
+
+const fidelity = ref('L3')
+const targets = reactive({ tavg_nm: 10, efficiency_pct: 92 })
+const resultsJson = ref('')
+const analyzing = ref(false)
+const analysis = ref<any>(null)
+const aiAnalysis = ref<any>(null)
+
+const confidence = computed(() => analysis.value?.confidence)
+const metricStats = computed(() => {
+  const stats = analysis.value?.quantitative?.statistics || {}
+  return Object.entries(stats).map(([metric, s]: any) => ({
+    metric, min: s.min, max: s.max, mean: s.mean,
+  }))
+})
+const convChecks = computed(() => {
+  const conv = analysis.value?.quantitative?.convergence || {}
+  return Object.entries(conv)
+    .filter(([k]) => k !== 'summary')
+    .map(([name, v]: any) => ({ name, passed: v.passed, value: v.value ?? '-' }))
+})
+
+const loadExample = () => {
+  const data = []
+  for (let i = 0; i < 15; i++) {
+    const airgap = 0.5 + Math.random() * 1.5
+    const current = 10 + Math.random() * 15
+    data.push({
+      airgap_mm: +airgap.toFixed(2),
+      current_a: +current.toFixed(1),
+      tavg_nm: +(current * 2.5 / Math.sqrt(airgap) + (Math.random() - 0.5)).toFixed(2),
+      efficiency_pct: +(88 + Math.random() * 5).toFixed(1),
+      total_losses_w: +(40 + Math.random() * 20).toFixed(1),
+      winding_temp_c: +(85 + Math.random() * 25).toFixed(1),
+      feasible: Math.random() > 0.15,
+    })
+  }
+  resultsJson.value = JSON.stringify(data, null, 2)
+}
+
+const doAnalyze = async () => {
+  let results: any[]
+  try {
+    results = JSON.parse(resultsJson.value)
+  } catch {
+    ElMessage.error('\u7ed3\u679c\u6570\u636e\u4e0d\u662f\u6709\u6548\u7684JSON')
+    return
+  }
+  if (!Array.isArray(results) || !results.length) {
+    ElMessage.error('\u8bf7\u8f93\u5165\u81f3\u5c11\u4e00\u6761\u7ed3\u679c\u6570\u636e')
+    return
+  }
+  analyzing.value = true
+  try {
+    const res: any = await analysisApi.analyze({
+      results,
+      targets: { ...targets },
+      fidelity: fidelity.value,
+    })
+    analysis.value = res
+    aiAnalysis.value = res.ai_analysis
+    ElMessage.success('\u5206\u6790\u5b8c\u6210')
+  } catch (e: any) {
+    ElMessage.error('\u5206\u6790\u5931\u8d25: ' + (e.message || e))
+  } finally {
+    analyzing.value = false
+  }
+}
+</script>
+
+<style scoped>
+.result-analysis { padding: 20px; }
+.title { font-weight: 600; font-size: 16px; }
+.card-header { display: flex; justify-content: space-between; align-items: center; }
+.input-area { display: flex; flex-direction: column; gap: 8px; }
+.input-label { font-weight: 600; color: #606266; font-size: 13px; }
+.actions { display: flex; gap: 8px; justify-content: flex-end; }
+.section-title { font-weight: 600; margin: 12px 0 8px; color: #303133; }
+.suggestions { margin-top: 16px; }
+</style>