2026-02-24 06:54:14 +00:00
|
|
|
|
<template>
|
2026-02-25 06:06:37 +00:00
|
|
|
|
<div class="rag-lab-page">
|
|
|
|
|
|
<div class="page-header">
|
|
|
|
|
|
<h1 class="page-title">RAG 实验室</h1>
|
|
|
|
|
|
<p class="page-desc">测试检索增强生成效果,查看检索结果和 AI 响应。</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<el-row :gutter="24">
|
|
|
|
|
|
<el-col :xs="24" :sm="24" :md="10" :lg="10">
|
|
|
|
|
|
<el-card shadow="hover" class="input-card">
|
|
|
|
|
|
<template #header>
|
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
<div class="header-left">
|
|
|
|
|
|
<div class="icon-wrapper">
|
|
|
|
|
|
<el-icon><Edit /></el-icon>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="header-title">调试输入</span>
|
|
|
|
|
|
</div>
|
2026-02-27 16:30:54 +00:00
|
|
|
|
<el-switch
|
|
|
|
|
|
v-model="flowTestMode"
|
|
|
|
|
|
active-text="完整流程测试"
|
|
|
|
|
|
inactive-text="RAG 测试"
|
|
|
|
|
|
@change="handleModeChange"
|
|
|
|
|
|
/>
|
2026-02-25 06:06:37 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-02-24 06:54:14 +00:00
|
|
|
|
<el-form label-position="top">
|
|
|
|
|
|
<el-form-item label="查询 Query">
|
|
|
|
|
|
<el-input
|
2026-02-25 06:45:17 +00:00
|
|
|
|
v-model="query"
|
2026-02-24 06:54:14 +00:00
|
|
|
|
type="textarea"
|
|
|
|
|
|
:rows="4"
|
|
|
|
|
|
placeholder="输入测试问题..."
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-form-item>
|
2026-02-27 16:30:54 +00:00
|
|
|
|
<el-form-item label="知识库范围" v-if="!flowTestMode">
|
2026-02-24 06:54:14 +00:00
|
|
|
|
<el-select
|
2026-02-25 06:45:17 +00:00
|
|
|
|
v-model="kbIds"
|
2026-02-24 06:54:14 +00:00
|
|
|
|
multiple
|
|
|
|
|
|
placeholder="请选择知识库"
|
|
|
|
|
|
style="width: 100%"
|
2026-02-24 11:52:52 +00:00
|
|
|
|
:loading="kbLoading"
|
2026-02-25 06:06:37 +00:00
|
|
|
|
:teleported="true"
|
|
|
|
|
|
:popper-options="{ modifiers: [{ name: 'flip', enabled: true }, { name: 'preventOverflow', enabled: true }] }"
|
2026-02-24 06:54:14 +00:00
|
|
|
|
>
|
2026-02-24 11:52:52 +00:00
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="kb in knowledgeBases"
|
|
|
|
|
|
:key="kb.id"
|
|
|
|
|
|
:label="`${kb.name} (${kb.documentCount}个文档)`"
|
|
|
|
|
|
:value="kb.id"
|
|
|
|
|
|
/>
|
2026-02-24 06:54:14 +00:00
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
2026-02-27 16:30:54 +00:00
|
|
|
|
<el-form-item label="LLM 模型" v-if="!flowTestMode">
|
2026-02-25 06:06:37 +00:00
|
|
|
|
<LLMSelector
|
2026-02-25 06:45:17 +00:00
|
|
|
|
v-model="llmProvider"
|
2026-02-25 06:06:37 +00:00
|
|
|
|
:providers="llmProviders"
|
|
|
|
|
|
:loading="llmLoading"
|
|
|
|
|
|
:current-provider="currentLLMProvider"
|
|
|
|
|
|
placeholder="使用默认配置"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
@change="handleLLMChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-form-item>
|
2026-02-27 16:30:54 +00:00
|
|
|
|
|
|
|
|
|
|
<template v-if="flowTestMode">
|
|
|
|
|
|
<el-divider content-position="left">流程配置</el-divider>
|
|
|
|
|
|
<div class="flow-config">
|
|
|
|
|
|
<div class="config-item">
|
|
|
|
|
|
<span class="config-label">意图识别</span>
|
|
|
|
|
|
<el-switch v-model="flowConfig.enable_intent" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="config-item">
|
|
|
|
|
|
<span class="config-label">话术流程</span>
|
|
|
|
|
|
<el-switch v-model="flowConfig.enable_flow" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="config-item">
|
|
|
|
|
|
<span class="config-label">RAG 检索</span>
|
|
|
|
|
|
<el-switch v-model="flowConfig.enable_rag" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="config-item">
|
|
|
|
|
|
<span class="config-label">输出护栏</span>
|
|
|
|
|
|
<el-switch v-model="flowConfig.enable_guardrail" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="config-item">
|
|
|
|
|
|
<span class="config-label">上下文记忆</span>
|
|
|
|
|
|
<el-switch v-model="flowConfig.enable_memory" />
|
|
|
|
|
|
</div>
|
2026-02-25 06:06:37 +00:00
|
|
|
|
</div>
|
2026-02-27 16:30:54 +00:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-if="!flowTestMode">
|
|
|
|
|
|
<el-form-item label="参数配置">
|
|
|
|
|
|
<div class="param-item">
|
|
|
|
|
|
<span class="label">Top-K</span>
|
|
|
|
|
|
<el-input-number v-model="topK" :min="1" :max="10" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="param-item">
|
|
|
|
|
|
<span class="label">Score Threshold</span>
|
|
|
|
|
|
<el-slider
|
|
|
|
|
|
v-model="scoreThreshold"
|
|
|
|
|
|
:min="0"
|
|
|
|
|
|
:max="1"
|
|
|
|
|
|
:step="0.1"
|
|
|
|
|
|
show-input
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="param-item">
|
|
|
|
|
|
<span class="label">生成 AI 回复</span>
|
|
|
|
|
|
<el-switch v-model="generateResponse" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="param-item" v-if="generateResponse">
|
|
|
|
|
|
<span class="label">流式输出</span>
|
|
|
|
|
|
<el-switch v-model="streamOutput" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-02-25 06:06:37 +00:00
|
|
|
|
<el-button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
block
|
|
|
|
|
|
@click="handleRun"
|
|
|
|
|
|
:loading="loading || streaming"
|
|
|
|
|
|
>
|
2026-02-27 16:30:54 +00:00
|
|
|
|
{{ flowTestMode ? '执行流程测试' : (streaming ? '生成中...' : '运行实验') }}
|
2026-02-25 06:06:37 +00:00
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="streaming"
|
|
|
|
|
|
type="danger"
|
|
|
|
|
|
block
|
|
|
|
|
|
@click="handleStopStream"
|
|
|
|
|
|
style="margin-top: 10px;"
|
|
|
|
|
|
>
|
|
|
|
|
|
停止生成
|
2026-02-24 06:54:14 +00:00
|
|
|
|
</el-button>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
|
2026-02-25 06:06:37 +00:00
|
|
|
|
<el-col :xs="24" :sm="24" :md="14" :lg="14">
|
2026-02-27 16:30:54 +00:00
|
|
|
|
<template v-if="flowTestMode">
|
|
|
|
|
|
<el-card shadow="hover" class="result-card" v-loading="loading">
|
|
|
|
|
|
<template #header>
|
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
<div class="header-left">
|
|
|
|
|
|
<div class="icon-wrapper success">
|
|
|
|
|
|
<el-icon><Share /></el-icon>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="header-title">执行流程 (12步)</span>
|
2026-02-24 06:54:14 +00:00
|
|
|
|
</div>
|
2026-02-27 16:30:54 +00:00
|
|
|
|
<div class="header-right" v-if="flowTestResult">
|
|
|
|
|
|
<el-tag :type="getStatusType(flowTestResult.status)" size="small">
|
|
|
|
|
|
{{ flowTestResult.status }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
<span class="duration">{{ flowTestResult.totalDurationMs }}ms</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="!flowTestResult" class="placeholder-text">
|
|
|
|
|
|
切换到"完整流程测试"模式,输入测试消息后点击执行
|
2026-02-24 11:52:52 +00:00
|
|
|
|
</div>
|
2026-02-27 16:30:54 +00:00
|
|
|
|
|
|
|
|
|
|
<div v-else class="flow-result">
|
|
|
|
|
|
<el-timeline>
|
|
|
|
|
|
<el-timeline-item
|
|
|
|
|
|
v-for="step in flowTestResult.steps"
|
|
|
|
|
|
:key="step.step"
|
|
|
|
|
|
:type="getStepStatusType(step.status)"
|
|
|
|
|
|
:hollow="step.status === 'skipped'"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-card shadow="never" class="step-card" @click="toggleStepDetail(step.step)">
|
|
|
|
|
|
<div class="step-header">
|
|
|
|
|
|
<div class="step-info">
|
|
|
|
|
|
<span class="step-number">Step {{ step.step }}</span>
|
|
|
|
|
|
<span class="step-name">{{ getStepName(step.name) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="step-meta">
|
|
|
|
|
|
<el-tag :type="getStepStatusType(step.status)" size="small" effect="plain">
|
|
|
|
|
|
{{ step.status }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
<span class="step-duration">{{ step.duration_ms }}ms</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="expandedSteps.includes(step.step)" class="step-detail">
|
|
|
|
|
|
<el-divider content-position="left">输入</el-divider>
|
|
|
|
|
|
<pre class="code-block"><code>{{ JSON.stringify(step.input, null, 2) }}</code></pre>
|
|
|
|
|
|
<el-divider content-position="left">输出</el-divider>
|
|
|
|
|
|
<pre class="code-block"><code>{{ JSON.stringify(step.output, null, 2) }}</code></pre>
|
|
|
|
|
|
<template v-if="step.error">
|
|
|
|
|
|
<el-divider content-position="left">错误</el-divider>
|
|
|
|
|
|
<el-alert type="error" :closable="false">{{ step.error }}</el-alert>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</el-timeline-item>
|
|
|
|
|
|
</el-timeline>
|
|
|
|
|
|
|
|
|
|
|
|
<el-divider content-position="left" v-if="flowTestResult.finalResponse">最终响应</el-divider>
|
|
|
|
|
|
<div v-if="flowTestResult.finalResponse" class="final-response">
|
|
|
|
|
|
<div class="response-content">{{ flowTestResult.finalResponse.reply }}</div>
|
|
|
|
|
|
<div class="response-meta">
|
|
|
|
|
|
<span v-if="flowTestResult.finalResponse.confidence">
|
|
|
|
|
|
置信度: {{ (flowTestResult.finalResponse.confidence * 100).toFixed(1) }}%
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<el-tag v-if="flowTestResult.finalResponse.should_transfer" type="warning" size="small">
|
|
|
|
|
|
需转人工
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-24 11:52:52 +00:00
|
|
|
|
</div>
|
2026-02-27 16:30:54 +00:00
|
|
|
|
</el-card>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<el-tabs v-model="activeTab" type="border-card" class="result-tabs">
|
|
|
|
|
|
<el-tab-pane label="召回片段" name="retrieval">
|
|
|
|
|
|
<div v-if="retrievalResults.length === 0" class="placeholder-text">
|
|
|
|
|
|
暂无实验数据
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="result-list">
|
|
|
|
|
|
<el-card
|
|
|
|
|
|
v-for="(item, index) in retrievalResults"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
class="result-card"
|
|
|
|
|
|
shadow="never"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="result-header">
|
|
|
|
|
|
<el-tag size="small" type="primary">Score: {{ item.score.toFixed(4) }}</el-tag>
|
|
|
|
|
|
<span class="source">来源: {{ item.source }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="result-content">{{ item.content }}</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
<el-tab-pane label="最终 Prompt" name="prompt">
|
|
|
|
|
|
<div v-if="!finalPrompt" class="placeholder-text">
|
|
|
|
|
|
等待实验运行...
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="prompt-view">
|
|
|
|
|
|
<pre><code>{{ finalPrompt }}</code></pre>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
<el-tab-pane label="AI 回复" name="ai-response" v-if="generateResponse">
|
|
|
|
|
|
<StreamOutput
|
|
|
|
|
|
v-if="streamOutput"
|
|
|
|
|
|
:content="streamContent"
|
|
|
|
|
|
:is-streaming="streaming"
|
|
|
|
|
|
:error="streamError"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<AIResponseViewer
|
|
|
|
|
|
v-else
|
|
|
|
|
|
:response="aiResponse"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
<el-tab-pane label="诊断信息" name="diagnostics">
|
|
|
|
|
|
<div v-if="!diagnostics" class="placeholder-text">
|
|
|
|
|
|
等待实验运行...
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="diagnostics-view">
|
|
|
|
|
|
<pre><code>{{ JSON.stringify(diagnostics, null, 2) }}</code></pre>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
</el-tabs>
|
|
|
|
|
|
</template>
|
2026-02-24 06:54:14 +00:00
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-02-27 16:30:54 +00:00
|
|
|
|
import { ref, reactive, onMounted } from 'vue'
|
2026-02-24 06:54:14 +00:00
|
|
|
|
import { ElMessage } from 'element-plus'
|
2026-02-27 16:30:54 +00:00
|
|
|
|
import { Edit, Share } from '@element-plus/icons-vue'
|
2026-02-25 06:06:37 +00:00
|
|
|
|
import { runRagExperiment, createSSEConnection, type AIResponse, type RetrievalResult } from '@/api/rag'
|
|
|
|
|
|
import { getLLMProviders, getLLMConfig, type LLMProviderInfo } from '@/api/llm'
|
2026-02-24 11:52:52 +00:00
|
|
|
|
import { listKnowledgeBases } from '@/api/kb'
|
2026-02-27 16:30:54 +00:00
|
|
|
|
import { executeFlowTest, type FlowExecutionResponse, type FlowExecutionStep } from '@/api/flow-test'
|
2026-02-25 06:45:17 +00:00
|
|
|
|
import { useRagLabStore } from '@/stores/ragLab'
|
|
|
|
|
|
import { storeToRefs } from 'pinia'
|
2026-02-25 06:06:37 +00:00
|
|
|
|
import AIResponseViewer from '@/components/rag/AIResponseViewer.vue'
|
|
|
|
|
|
import StreamOutput from '@/components/rag/StreamOutput.vue'
|
|
|
|
|
|
import LLMSelector from '@/components/rag/LLMSelector.vue'
|
2026-02-24 11:52:52 +00:00
|
|
|
|
|
|
|
|
|
|
interface KnowledgeBase {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
name: string
|
|
|
|
|
|
documentCount: number
|
|
|
|
|
|
}
|
2026-02-24 06:54:14 +00:00
|
|
|
|
|
2026-02-25 06:45:17 +00:00
|
|
|
|
const ragLabStore = useRagLabStore()
|
|
|
|
|
|
const {
|
|
|
|
|
|
query,
|
|
|
|
|
|
kbIds,
|
|
|
|
|
|
llmProvider,
|
|
|
|
|
|
topK,
|
|
|
|
|
|
scoreThreshold,
|
|
|
|
|
|
generateResponse,
|
|
|
|
|
|
streamOutput
|
|
|
|
|
|
} = storeToRefs(ragLabStore)
|
|
|
|
|
|
|
2026-02-24 06:54:14 +00:00
|
|
|
|
const loading = ref(false)
|
2026-02-24 11:52:52 +00:00
|
|
|
|
const kbLoading = ref(false)
|
2026-02-25 06:06:37 +00:00
|
|
|
|
const llmLoading = ref(false)
|
|
|
|
|
|
const streaming = ref(false)
|
2026-02-24 06:54:14 +00:00
|
|
|
|
const activeTab = ref('retrieval')
|
2026-02-24 11:52:52 +00:00
|
|
|
|
const knowledgeBases = ref<KnowledgeBase[]>([])
|
2026-02-25 06:06:37 +00:00
|
|
|
|
const llmProviders = ref<LLMProviderInfo[]>([])
|
|
|
|
|
|
const currentLLMProvider = ref('')
|
2026-02-24 06:54:14 +00:00
|
|
|
|
|
2026-02-25 06:06:37 +00:00
|
|
|
|
const retrievalResults = ref<RetrievalResult[]>([])
|
|
|
|
|
|
const finalPrompt = ref('')
|
|
|
|
|
|
const aiResponse = ref<AIResponse | null>(null)
|
|
|
|
|
|
const diagnostics = ref<any>(null)
|
|
|
|
|
|
const streamContent = ref('')
|
|
|
|
|
|
const streamError = ref<string | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
const totalLatencyMs = ref(0)
|
|
|
|
|
|
|
2026-02-27 16:30:54 +00:00
|
|
|
|
const flowTestMode = ref(false)
|
|
|
|
|
|
const flowTestResult = ref<FlowExecutionResponse | null>(null)
|
|
|
|
|
|
const expandedSteps = ref<number[]>([])
|
|
|
|
|
|
|
|
|
|
|
|
const flowConfig = reactive({
|
|
|
|
|
|
enable_intent: true,
|
|
|
|
|
|
enable_flow: true,
|
|
|
|
|
|
enable_rag: true,
|
|
|
|
|
|
enable_guardrail: true,
|
|
|
|
|
|
enable_memory: true
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-25 06:06:37 +00:00
|
|
|
|
let abortStream: (() => void) | null = null
|
2026-02-24 06:54:14 +00:00
|
|
|
|
|
2026-02-27 16:30:54 +00:00
|
|
|
|
const stepNameMap: Record<string, string> = {
|
|
|
|
|
|
'InputScanner': '输入扫描',
|
|
|
|
|
|
'FlowEngine': '流程引擎',
|
|
|
|
|
|
'IntentRouter': '意图路由',
|
|
|
|
|
|
'QueryRewriter': '查询重写',
|
|
|
|
|
|
'MultiKBRetrieval': '多知识库检索',
|
|
|
|
|
|
'ResultRanker': '结果排序',
|
|
|
|
|
|
'PromptBuilder': 'Prompt 构建',
|
|
|
|
|
|
'LLMGenerate': 'LLM 生成',
|
|
|
|
|
|
'OutputFilter': '输出过滤',
|
|
|
|
|
|
'Confidence': '置信度计算',
|
|
|
|
|
|
'Memory': '记忆存储',
|
|
|
|
|
|
'Response': '响应返回'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getStepName = (name: string) => {
|
|
|
|
|
|
return stepNameMap[name] || name
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getStatusType = (status: string) => {
|
|
|
|
|
|
switch (status) {
|
|
|
|
|
|
case 'success': return 'success'
|
|
|
|
|
|
case 'failed': return 'danger'
|
|
|
|
|
|
case 'partial': return 'warning'
|
|
|
|
|
|
default: return 'info'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getStepStatusType = (status: string) => {
|
|
|
|
|
|
switch (status) {
|
|
|
|
|
|
case 'success': return 'success'
|
|
|
|
|
|
case 'failed': return 'danger'
|
|
|
|
|
|
case 'skipped': return 'info'
|
|
|
|
|
|
default: return 'warning'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const toggleStepDetail = (step: number) => {
|
|
|
|
|
|
const index = expandedSteps.value.indexOf(step)
|
|
|
|
|
|
if (index > -1) {
|
|
|
|
|
|
expandedSteps.value.splice(index, 1)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
expandedSteps.value.push(step)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleModeChange = () => {
|
|
|
|
|
|
flowTestResult.value = null
|
|
|
|
|
|
expandedSteps.value = []
|
|
|
|
|
|
clearResults()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 11:52:52 +00:00
|
|
|
|
const fetchKnowledgeBases = async () => {
|
|
|
|
|
|
kbLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res: any = await listKnowledgeBases()
|
|
|
|
|
|
knowledgeBases.value = res.data || []
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to fetch knowledge bases:', error)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
kbLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 06:06:37 +00:00
|
|
|
|
const fetchLLMProviders = async () => {
|
|
|
|
|
|
llmLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const [providersRes, configRes]: [any, any] = await Promise.all([
|
|
|
|
|
|
getLLMProviders(),
|
|
|
|
|
|
getLLMConfig()
|
|
|
|
|
|
])
|
|
|
|
|
|
llmProviders.value = providersRes?.providers || []
|
|
|
|
|
|
currentLLMProvider.value = configRes?.provider || ''
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to fetch LLM providers:', error)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
llmLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleLLMChange = (provider: LLMProviderInfo | undefined) => {
|
2026-02-25 06:45:17 +00:00
|
|
|
|
llmProvider.value = provider?.name || ''
|
2026-02-25 06:06:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 06:54:14 +00:00
|
|
|
|
const handleRun = async () => {
|
2026-02-25 06:45:17 +00:00
|
|
|
|
if (!query.value.trim()) {
|
2026-02-24 06:54:14 +00:00
|
|
|
|
ElMessage.warning('请输入查询 Query')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-02-25 06:06:37 +00:00
|
|
|
|
|
2026-02-27 16:30:54 +00:00
|
|
|
|
if (flowTestMode.value) {
|
|
|
|
|
|
await runFlowTest()
|
2026-02-25 06:06:37 +00:00
|
|
|
|
} else {
|
2026-02-27 16:30:54 +00:00
|
|
|
|
clearResults()
|
|
|
|
|
|
if (streamOutput.value && generateResponse.value) {
|
|
|
|
|
|
await runStreamExperiment()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await runNormalExperiment()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const runFlowTest = async () => {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
flowTestResult.value = null
|
|
|
|
|
|
expandedSteps.value = []
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await executeFlowTest({
|
|
|
|
|
|
message: query.value,
|
|
|
|
|
|
enable_flow: flowConfig.enable_flow,
|
|
|
|
|
|
enable_intent: flowConfig.enable_intent,
|
|
|
|
|
|
enable_rag: flowConfig.enable_rag,
|
|
|
|
|
|
enable_guardrail: flowConfig.enable_guardrail,
|
|
|
|
|
|
enable_memory: flowConfig.enable_memory
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
flowTestResult.value = result
|
|
|
|
|
|
ElMessage.success('流程测试完成')
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
console.error(err)
|
|
|
|
|
|
ElMessage.error(err?.message || '流程测试失败')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
2026-02-25 06:06:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const runNormalExperiment = async () => {
|
2026-02-24 06:54:14 +00:00
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
2026-02-25 06:06:37 +00:00
|
|
|
|
const res: any = await runRagExperiment({
|
2026-02-25 06:45:17 +00:00
|
|
|
|
query: query.value,
|
|
|
|
|
|
kb_ids: kbIds.value,
|
|
|
|
|
|
top_k: topK.value,
|
|
|
|
|
|
score_threshold: scoreThreshold.value,
|
|
|
|
|
|
llm_provider: llmProvider.value || undefined,
|
|
|
|
|
|
generate_response: generateResponse.value
|
2026-02-25 06:06:37 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
retrievalResults.value = res.retrieval_results || res.retrievalResults || []
|
|
|
|
|
|
finalPrompt.value = res.final_prompt || res.finalPrompt || ''
|
|
|
|
|
|
aiResponse.value = res.ai_response || res.aiResponse || null
|
|
|
|
|
|
diagnostics.value = res.diagnostics || null
|
|
|
|
|
|
totalLatencyMs.value = res.total_latency_ms || res.totalLatencyMs || 0
|
|
|
|
|
|
|
2026-02-25 06:45:17 +00:00
|
|
|
|
if (generateResponse.value) {
|
2026-02-25 06:06:37 +00:00
|
|
|
|
activeTab.value = 'ai-response'
|
|
|
|
|
|
} else {
|
|
|
|
|
|
activeTab.value = 'retrieval'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 06:54:14 +00:00
|
|
|
|
ElMessage.success('实验运行成功')
|
2026-02-25 06:06:37 +00:00
|
|
|
|
} catch (err: any) {
|
2026-02-24 06:54:14 +00:00
|
|
|
|
console.error(err)
|
2026-02-25 06:06:37 +00:00
|
|
|
|
ElMessage.error(err?.message || '实验运行失败')
|
2026-02-24 06:54:14 +00:00
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-24 11:52:52 +00:00
|
|
|
|
|
2026-02-25 06:06:37 +00:00
|
|
|
|
const runStreamExperiment = async () => {
|
|
|
|
|
|
streaming.value = true
|
|
|
|
|
|
streamContent.value = ''
|
|
|
|
|
|
streamError.value = null
|
|
|
|
|
|
activeTab.value = 'ai-response'
|
|
|
|
|
|
|
|
|
|
|
|
abortStream = createSSEConnection(
|
|
|
|
|
|
'/admin/rag/experiments/stream',
|
|
|
|
|
|
{
|
2026-02-25 06:45:17 +00:00
|
|
|
|
query: query.value,
|
|
|
|
|
|
kb_ids: kbIds.value,
|
|
|
|
|
|
top_k: topK.value,
|
|
|
|
|
|
score_threshold: scoreThreshold.value,
|
|
|
|
|
|
llm_provider: llmProvider.value || undefined,
|
2026-02-25 06:06:37 +00:00
|
|
|
|
generate_response: true
|
|
|
|
|
|
},
|
|
|
|
|
|
(data: string) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsed = JSON.parse(data)
|
|
|
|
|
|
|
|
|
|
|
|
if (parsed.type === 'content') {
|
|
|
|
|
|
streamContent.value += parsed.content || ''
|
|
|
|
|
|
} else if (parsed.type === 'retrieval') {
|
|
|
|
|
|
retrievalResults.value = parsed.results || []
|
|
|
|
|
|
} else if (parsed.type === 'prompt') {
|
|
|
|
|
|
finalPrompt.value = parsed.prompt || ''
|
|
|
|
|
|
} else if (parsed.type === 'complete') {
|
|
|
|
|
|
aiResponse.value = {
|
|
|
|
|
|
content: streamContent.value,
|
|
|
|
|
|
prompt_tokens: parsed.prompt_tokens,
|
|
|
|
|
|
completion_tokens: parsed.completion_tokens,
|
|
|
|
|
|
total_tokens: parsed.total_tokens,
|
|
|
|
|
|
latency_ms: parsed.latency_ms,
|
|
|
|
|
|
model: parsed.model
|
|
|
|
|
|
}
|
|
|
|
|
|
totalLatencyMs.value = parsed.total_latency_ms || 0
|
|
|
|
|
|
streaming.value = false
|
|
|
|
|
|
ElMessage.success('生成完成')
|
|
|
|
|
|
} else if (parsed.type === 'error') {
|
|
|
|
|
|
streamError.value = parsed.message || '流式输出错误'
|
|
|
|
|
|
streaming.value = false
|
2026-02-25 18:19:51 +00:00
|
|
|
|
ElMessage.error(streamError.value || '未知错误')
|
2026-02-25 06:06:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
streamContent.value += data
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
(error: Error) => {
|
|
|
|
|
|
streaming.value = false
|
|
|
|
|
|
streamError.value = error.message
|
|
|
|
|
|
ElMessage.error(error.message)
|
|
|
|
|
|
},
|
|
|
|
|
|
() => {
|
|
|
|
|
|
streaming.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleStopStream = () => {
|
|
|
|
|
|
if (abortStream) {
|
|
|
|
|
|
abortStream()
|
|
|
|
|
|
abortStream = null
|
|
|
|
|
|
}
|
|
|
|
|
|
streaming.value = false
|
|
|
|
|
|
ElMessage.info('已停止生成')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const clearResults = () => {
|
|
|
|
|
|
retrievalResults.value = []
|
|
|
|
|
|
finalPrompt.value = ''
|
|
|
|
|
|
aiResponse.value = null
|
|
|
|
|
|
diagnostics.value = null
|
|
|
|
|
|
streamContent.value = ''
|
|
|
|
|
|
streamError.value = null
|
|
|
|
|
|
totalLatencyMs.value = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 11:52:52 +00:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
fetchKnowledgeBases()
|
2026-02-25 06:06:37 +00:00
|
|
|
|
fetchLLMProviders()
|
2026-02-24 11:52:52 +00:00
|
|
|
|
})
|
2026-02-24 06:54:14 +00:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-02-25 06:06:37 +00:00
|
|
|
|
.rag-lab-page {
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
min-height: calc(100vh - 60px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.page-header {
|
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.page-title {
|
|
|
|
|
|
margin: 0 0 8px 0;
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
letter-spacing: -0.5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.page-desc {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.input-card {
|
|
|
|
|
|
animation: fadeInUp 0.5s ease-out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes fadeInUp {
|
|
|
|
|
|
from {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateY(20px);
|
|
|
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header-left {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.icon-wrapper {
|
|
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
background-color: var(--primary-lighter);
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
color: var(--primary-color);
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 16:30:54 +00:00
|
|
|
|
.icon-wrapper.success {
|
|
|
|
|
|
background-color: #D1FAE5;
|
|
|
|
|
|
color: #059669;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 06:06:37 +00:00
|
|
|
|
.header-title {
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 16:30:54 +00:00
|
|
|
|
.header-right {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.duration {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.flow-config {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.config-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
background-color: var(--bg-tertiary);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.config-label {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 06:54:14 +00:00
|
|
|
|
.param-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-02-25 06:06:37 +00:00
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
gap: 16px;
|
2026-02-24 06:54:14 +00:00
|
|
|
|
}
|
2026-02-25 06:06:37 +00:00
|
|
|
|
|
2026-02-24 06:54:14 +00:00
|
|
|
|
.param-item .label {
|
2026-02-25 06:06:37 +00:00
|
|
|
|
width: 140px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.param-item :deep(.el-slider) {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.result-tabs {
|
|
|
|
|
|
animation: fadeInUp 0.6s ease-out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.result-tabs :deep(.el-tabs__header) {
|
|
|
|
|
|
border-radius: 12px 12px 0 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.placeholder-text {
|
|
|
|
|
|
color: var(--text-tertiary);
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 60px 20px;
|
2026-02-24 06:54:14 +00:00
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
2026-02-25 06:06:37 +00:00
|
|
|
|
|
|
|
|
|
|
.result-list {
|
|
|
|
|
|
max-height: 600px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
padding-right: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.result-card {
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 06:54:14 +00:00
|
|
|
|
.result-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
2026-02-25 06:06:37 +00:00
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.source {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--text-tertiary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.result-content {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.prompt-view,
|
|
|
|
|
|
.diagnostics-view {
|
|
|
|
|
|
background-color: var(--bg-tertiary);
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
border-radius: 10px;
|
2026-02-24 06:54:14 +00:00
|
|
|
|
max-height: 600px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
2026-02-25 06:06:37 +00:00
|
|
|
|
|
|
|
|
|
|
.prompt-view pre,
|
|
|
|
|
|
.diagnostics-view pre {
|
2026-02-24 06:54:14 +00:00
|
|
|
|
margin: 0;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
word-wrap: break-word;
|
2026-02-25 06:06:37 +00:00
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 16:30:54 +00:00
|
|
|
|
.flow-result {
|
|
|
|
|
|
max-height: 700px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-card {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-card:hover {
|
|
|
|
|
|
background-color: var(--bg-tertiary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-number {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--text-tertiary);
|
|
|
|
|
|
background-color: var(--bg-tertiary);
|
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-name {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-meta {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-duration {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--text-tertiary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-detail {
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.code-block {
|
|
|
|
|
|
background-color: var(--bg-tertiary);
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.code-block code {
|
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
word-wrap: break-word;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.final-response {
|
|
|
|
|
|
background-color: var(--bg-tertiary);
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.response-content {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.response-meta {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 06:06:37 +00:00
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
.rag-lab-page {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.page-title {
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.param-item {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.param-item .label {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
2026-02-27 16:30:54 +00:00
|
|
|
|
|
|
|
|
|
|
.flow-config {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
2026-02-24 06:54:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|