mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
style(debug): unify lab workspace card surfaces
This commit is contained in:
1 parent
12c7fc2105
commit
8f1f9a56ac
3 files changed
+277
-252
No files matched your search
@@ -5,7 +5,7 @@ import type { TableSchema, SQLResult } from '@/components/analysis/SQLLab'
|
||||
import { useDataService, useAIService } from '@/services'
|
||||
import { getColumnLabel } from '@/components/analysis/SQLLab'
|
||||
import type { LocaleType } from '@/i18n/types'
|
||||
import { UITabs } from '@/components/UI'
|
||||
import { ThemeCard, UITabs } from '@/components/UI'
|
||||
|
||||
const PRIORITY_TABLES = ['member', 'meta', 'message', 'message_context', 'member_name_history']
|
||||
|
||||
@@ -333,165 +333,167 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<!-- Toolbar -->
|
||||
<div class="relative z-20 flex items-center gap-2 border-b border-gray-200 px-4 py-2 dark:border-gray-700">
|
||||
<!-- DB Source Tabs -->
|
||||
<UITabs v-model="dbSource" :items="dbTabs" size="xs" />
|
||||
<div class="h-full p-3">
|
||||
<ThemeCard class="flex h-full min-w-0 flex-col">
|
||||
<!-- Toolbar -->
|
||||
<div class="relative z-20 flex items-center gap-2 border-b border-gray-200 px-4 py-2 dark:border-gray-700">
|
||||
<!-- DB Source Tabs -->
|
||||
<UITabs v-model="dbSource" :items="dbTabs" size="xs" />
|
||||
|
||||
<div class="mx-1 h-4 w-px bg-gray-200 dark:bg-gray-700" />
|
||||
<div class="mx-1 h-4 w-px bg-gray-200 dark:bg-gray-700" />
|
||||
|
||||
<!-- Table Tabs -->
|
||||
<div :class="isAiDb ? 'max-w-[50%]' : 'flex-1'" class="min-w-0">
|
||||
<UITabs v-model="selectedTable" :items="tableItems" size="xs" />
|
||||
<!-- Table Tabs -->
|
||||
<div :class="isAiDb ? 'max-w-[50%]' : 'flex-1'" class="min-w-0">
|
||||
<UITabs v-model="selectedTable" :items="tableItems" size="xs" />
|
||||
</div>
|
||||
|
||||
<!-- Conversation Filter Dropdown (AI DB + ai_message) -->
|
||||
<USelectMenu
|
||||
v-if="showConversationFilter"
|
||||
v-model="selectedAIChat"
|
||||
:items="conversationItems"
|
||||
value-key="value"
|
||||
class="w-48"
|
||||
size="xs"
|
||||
:ui="{ content: 'z-50' }"
|
||||
/>
|
||||
|
||||
<div class="flex-1" />
|
||||
|
||||
<UButton
|
||||
v-if="isAiDb && selectedTable === 'ai_message'"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
color="error"
|
||||
icon="i-heroicons-trash"
|
||||
:loading="isClearingDebug"
|
||||
@click="handleClearDebugContext"
|
||||
>
|
||||
{{ t('analysis.debug.tableBrowser.clearDebugContext') }}
|
||||
</UButton>
|
||||
|
||||
<UButton variant="ghost" size="xs" icon="i-heroicons-arrow-path" :loading="isLoading" @click="loadTableData">
|
||||
{{ t('analysis.debug.tableBrowser.refresh') }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- Conversation Filter Dropdown (AI DB + ai_message) -->
|
||||
<USelectMenu
|
||||
v-if="showConversationFilter"
|
||||
v-model="selectedAIChat"
|
||||
:items="conversationItems"
|
||||
value-key="value"
|
||||
class="w-48"
|
||||
size="xs"
|
||||
:ui="{ content: 'z-50' }"
|
||||
/>
|
||||
|
||||
<div class="flex-1" />
|
||||
|
||||
<UButton
|
||||
v-if="isAiDb && selectedTable === 'ai_message'"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
color="error"
|
||||
icon="i-heroicons-trash"
|
||||
:loading="isClearingDebug"
|
||||
@click="handleClearDebugContext"
|
||||
<!-- Error -->
|
||||
<div
|
||||
v-if="error"
|
||||
class="mx-4 mt-2 rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-900/20 dark:text-red-400"
|
||||
>
|
||||
{{ t('analysis.debug.tableBrowser.clearDebugContext') }}
|
||||
</UButton>
|
||||
|
||||
<UButton variant="ghost" size="xs" icon="i-heroicons-arrow-path" :loading="isLoading" @click="loadTableData">
|
||||
{{ t('analysis.debug.tableBrowser.refresh') }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- Error -->
|
||||
<div
|
||||
v-if="error"
|
||||
class="mx-4 mt-2 rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-900/20 dark:text-red-400"
|
||||
>
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<!-- Table Content -->
|
||||
<div class="flex-1 overflow-auto">
|
||||
<div v-if="!result && !isLoading" class="flex h-full items-center justify-center">
|
||||
<p class="text-sm text-gray-400">{{ t('analysis.debug.tableBrowser.noData') }}</p>
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<table v-else-if="result" class="w-full border-collapse text-xs">
|
||||
<thead class="sticky top-0 bg-gray-50 dark:bg-page-dark">
|
||||
<tr>
|
||||
<th
|
||||
class="whitespace-nowrap border-b border-r border-gray-200 px-2 py-1.5 text-left font-medium text-gray-500 dark:border-gray-700 dark:text-gray-400"
|
||||
>
|
||||
#
|
||||
</th>
|
||||
<th
|
||||
v-for="col in result.columns"
|
||||
:key="col"
|
||||
class="border-b border-r border-gray-200 px-2 py-1.5 text-left dark:border-gray-700"
|
||||
>
|
||||
<div class="font-mono text-xs font-medium text-gray-700 dark:text-gray-300">{{ col }}</div>
|
||||
<div class="text-[10px] leading-tight text-gray-400">
|
||||
{{ getColumnLabel(selectedTable, col, locale as LocaleType) }}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
class="whitespace-nowrap border-b border-gray-200 px-2 py-1.5 text-center font-medium text-gray-500 dark:border-gray-700 dark:text-gray-400"
|
||||
>
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(row, rowIdx) in result.rows"
|
||||
:key="rowIdx"
|
||||
class="transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||
>
|
||||
<td class="border-b border-r border-gray-100 px-2 py-1.5 text-gray-400 dark:border-gray-800">
|
||||
{{ (currentPage - 1) * pageSize + rowIdx + 1 }}
|
||||
</td>
|
||||
<td
|
||||
v-for="(cell, colIdx) in row"
|
||||
:key="colIdx"
|
||||
class="max-w-[300px] border-b border-r border-gray-100 p-0 text-gray-700 dark:border-gray-800 dark:text-gray-300"
|
||||
:title="cell == null ? 'NULL' : String(cell)"
|
||||
>
|
||||
<template v-if="editingRowIndex === rowIdx">
|
||||
<input
|
||||
v-model="editingValues[result.columns[colIdx]]"
|
||||
class="w-full px-2 py-1.5 rounded border border-gray-300 bg-white text-xs dark:border-gray-600 dark:bg-gray-800"
|
||||
/>
|
||||
</template>
|
||||
<UContextMenu v-else :items="getCellContextMenu(cell, colIdx, rowIdx)">
|
||||
<div class="w-full truncate px-2 py-1.5">
|
||||
<span v-if="cell == null" class="italic text-gray-400">NULL</span>
|
||||
<span v-else>{{ String(cell).slice(0, 200) }}</span>
|
||||
<!-- Table Content -->
|
||||
<div class="flex-1 overflow-auto">
|
||||
<div v-if="!result && !isLoading" class="flex h-full items-center justify-center">
|
||||
<p class="text-sm text-gray-400">{{ t('analysis.debug.tableBrowser.noData') }}</p>
|
||||
</div>
|
||||
|
||||
<table v-else-if="result" class="w-full border-collapse text-xs">
|
||||
<thead class="sticky top-0 bg-gray-50 dark:bg-page-dark">
|
||||
<tr>
|
||||
<th
|
||||
class="whitespace-nowrap border-b border-r border-gray-200 px-2 py-1.5 text-left font-medium text-gray-500 dark:border-gray-700 dark:text-gray-400"
|
||||
>
|
||||
#
|
||||
</th>
|
||||
<th
|
||||
v-for="col in result.columns"
|
||||
:key="col"
|
||||
class="border-b border-r border-gray-200 px-2 py-1.5 text-left dark:border-gray-700"
|
||||
>
|
||||
<div class="font-mono text-xs font-medium text-gray-700 dark:text-gray-300">{{ col }}</div>
|
||||
<div class="text-[10px] leading-tight text-gray-400">
|
||||
{{ getColumnLabel(selectedTable, col, locale as LocaleType) }}
|
||||
</div>
|
||||
</UContextMenu>
|
||||
</td>
|
||||
<td class="border-b border-gray-100 px-2 py-1.5 text-center dark:border-gray-800">
|
||||
<template v-if="editingRowIndex === rowIdx">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<UButton size="xs" color="primary" variant="ghost" icon="i-heroicons-check" @click="saveEdit" />
|
||||
<UButton size="xs" color="neutral" variant="ghost" icon="i-heroicons-x-mark" @click="cancelEdit" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<UButton size="xs" variant="ghost" icon="i-heroicons-pencil-square" @click="startEdit(rowIdx)" />
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
color="error"
|
||||
icon="i-heroicons-trash"
|
||||
@click="confirmDelete(rowIdx)"
|
||||
</th>
|
||||
<th
|
||||
class="whitespace-nowrap border-b border-gray-200 px-2 py-1.5 text-center font-medium text-gray-500 dark:border-gray-700 dark:text-gray-400"
|
||||
>
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(row, rowIdx) in result.rows"
|
||||
:key="rowIdx"
|
||||
class="transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||
>
|
||||
<td class="border-b border-r border-gray-100 px-2 py-1.5 text-gray-400 dark:border-gray-800">
|
||||
{{ (currentPage - 1) * pageSize + rowIdx + 1 }}
|
||||
</td>
|
||||
<td
|
||||
v-for="(cell, colIdx) in row"
|
||||
:key="colIdx"
|
||||
class="max-w-[300px] border-b border-r border-gray-100 p-0 text-gray-700 dark:border-gray-800 dark:text-gray-300"
|
||||
:title="cell == null ? 'NULL' : String(cell)"
|
||||
>
|
||||
<template v-if="editingRowIndex === rowIdx">
|
||||
<input
|
||||
v-model="editingValues[result.columns[colIdx]]"
|
||||
class="w-full px-2 py-1.5 rounded border border-gray-300 bg-white text-xs dark:border-gray-600 dark:bg-gray-800"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Footer: Row Count + Pagination -->
|
||||
<div class="flex items-center justify-between border-t border-gray-200 px-4 py-2 dark:border-gray-700">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ totalRows > 0 ? t('analysis.debug.tableBrowser.rowCount', { count: totalRows }) : '' }}
|
||||
</span>
|
||||
<div v-if="totalPages > 1" class="flex items-center gap-3">
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:disabled="currentPage <= 1"
|
||||
icon="i-heroicons-chevron-left"
|
||||
@click="currentPage--"
|
||||
/>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('analysis.debug.tableBrowser.page', { current: currentPage, total: totalPages }) }}
|
||||
</span>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:disabled="currentPage >= totalPages"
|
||||
icon="i-heroicons-chevron-right"
|
||||
@click="currentPage++"
|
||||
/>
|
||||
</template>
|
||||
<UContextMenu v-else :items="getCellContextMenu(cell, colIdx, rowIdx)">
|
||||
<div class="w-full truncate px-2 py-1.5">
|
||||
<span v-if="cell == null" class="italic text-gray-400">NULL</span>
|
||||
<span v-else>{{ String(cell).slice(0, 200) }}</span>
|
||||
</div>
|
||||
</UContextMenu>
|
||||
</td>
|
||||
<td class="border-b border-gray-100 px-2 py-1.5 text-center dark:border-gray-800">
|
||||
<template v-if="editingRowIndex === rowIdx">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<UButton size="xs" color="primary" variant="ghost" icon="i-heroicons-check" @click="saveEdit" />
|
||||
<UButton size="xs" color="neutral" variant="ghost" icon="i-heroicons-x-mark" @click="cancelEdit" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<UButton size="xs" variant="ghost" icon="i-heroicons-pencil-square" @click="startEdit(rowIdx)" />
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
color="error"
|
||||
icon="i-heroicons-trash"
|
||||
@click="confirmDelete(rowIdx)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer: Row Count + Pagination -->
|
||||
<div class="flex items-center justify-between border-t border-gray-200 px-4 py-2 dark:border-gray-700">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ totalRows > 0 ? t('analysis.debug.tableBrowser.rowCount', { count: totalRows }) : '' }}
|
||||
</span>
|
||||
<div v-if="totalPages > 1" class="flex items-center gap-3">
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:disabled="currentPage <= 1"
|
||||
icon="i-heroicons-chevron-left"
|
||||
@click="currentPage--"
|
||||
/>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('analysis.debug.tableBrowser.page', { current: currentPage, total: totalPages }) }}
|
||||
</span>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:disabled="currentPage >= totalPages"
|
||||
icon="i-heroicons-chevron-right"
|
||||
@click="currentPage++"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ThemeCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAIService } from '@/services'
|
||||
import { ThemeCard } from '@/components/UI'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -162,7 +163,7 @@ async function cancel() {
|
||||
<div class="flex h-full">
|
||||
<!-- Left Sidebar: Tool List -->
|
||||
<div
|
||||
class="w-56 shrink-0 overflow-y-auto border-r border-gray-200 bg-gray-50/50 dark:border-gray-700 dark:bg-page-dark/50"
|
||||
class="m-3 h-[calc(100%-1.5rem)] w-56 shrink-0 overflow-y-auto rounded-lg bg-white shadow-elevated dark:bg-sidebar-dark"
|
||||
>
|
||||
<!-- Core Tools Group -->
|
||||
<div v-if="coreTools.length > 0" class="py-2">
|
||||
@@ -210,101 +211,103 @@ async function cancel() {
|
||||
</div>
|
||||
|
||||
<!-- Right Content: Parameters + Result -->
|
||||
<div class="flex flex-1 flex-col gap-4 overflow-y-auto p-4">
|
||||
<!-- Tool Description -->
|
||||
<div
|
||||
v-if="selectedTool"
|
||||
class="rounded-lg border border-primary-200 bg-primary-50/50 px-4 py-3 dark:border-primary-800 dark:bg-primary-900/20"
|
||||
>
|
||||
<div class="text-sm font-medium text-primary-700 dark:text-primary-300">
|
||||
{{ toolLabel(selectedTool.name) }}
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-primary-600/70 dark:text-primary-400/70">
|
||||
{{ selectedTool.description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parameters Form -->
|
||||
<div v-if="paramFields.length > 0" class="flex flex-col gap-3">
|
||||
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('ai.lab.toolTest.parameters') }}
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div v-for="field in paramFields" :key="field.name" class="flex flex-col gap-1">
|
||||
<label class="text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ field.name }}
|
||||
<span v-if="field.required" class="text-red-500">*</span>
|
||||
<span v-if="field.type === 'number'" class="ml-1 text-gray-400">(number)</span>
|
||||
<span v-if="field.isArray" class="ml-1 text-gray-400">({{ field.arrayItemType }}[])</span>
|
||||
</label>
|
||||
<USelectMenu
|
||||
v-if="field.enumValues"
|
||||
v-model="paramValues[field.name]"
|
||||
:items="field.enumValues"
|
||||
:placeholder="field.description"
|
||||
size="sm"
|
||||
/>
|
||||
<UInput
|
||||
v-else
|
||||
v-model="paramValues[field.name]"
|
||||
:placeholder="
|
||||
field.isArray
|
||||
? t('ai.lab.toolTest.arrayPlaceholder')
|
||||
: field.defaultValue !== undefined
|
||||
? String(field.defaultValue)
|
||||
: field.description
|
||||
"
|
||||
:type="field.type === 'number' ? 'number' : 'text'"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Execute / Cancel Button -->
|
||||
<div class="flex items-center gap-3">
|
||||
<UButton
|
||||
v-if="!isExecuting"
|
||||
color="primary"
|
||||
:disabled="!selectedToolName"
|
||||
icon="i-heroicons-play"
|
||||
@click="execute"
|
||||
>
|
||||
{{ t('ai.lab.toolTest.execute') }}
|
||||
</UButton>
|
||||
<UButton v-else color="error" icon="i-heroicons-stop" @click="cancel">
|
||||
{{ t('ai.lab.toolTest.cancel') }}
|
||||
</UButton>
|
||||
<UIcon v-if="isExecuting" name="i-heroicons-arrow-path" class="h-4 w-4 animate-spin text-gray-400" />
|
||||
<span v-if="elapsed !== null" class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('ai.lab.toolTest.elapsed', { ms: elapsed }) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Result -->
|
||||
<div v-if="resultJson || resultError" class="flex-1 min-h-0">
|
||||
<h3 class="mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('ai.lab.toolTest.result') }}
|
||||
</h3>
|
||||
<ThemeCard class="my-3 mr-3 flex min-w-0 flex-1 flex-col">
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4">
|
||||
<!-- Tool Description -->
|
||||
<div
|
||||
v-if="resultError"
|
||||
class="rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-900/20 dark:text-red-400"
|
||||
v-if="selectedTool"
|
||||
class="rounded-lg border border-primary-200 bg-primary-50/50 px-4 py-3 dark:border-primary-800 dark:bg-primary-900/20"
|
||||
>
|
||||
{{ resultError }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="resultTruncated"
|
||||
class="mb-2 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-400"
|
||||
>
|
||||
{{ t('ai.lab.toolTest.truncated') }}
|
||||
<div class="text-sm font-medium text-primary-700 dark:text-primary-300">
|
||||
{{ toolLabel(selectedTool.name) }}
|
||||
</div>
|
||||
<pre
|
||||
class="max-h-[60vh] overflow-auto rounded-lg border border-gray-200 bg-gray-50 p-3 text-xs leading-relaxed text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200"
|
||||
>{{ resultJson }}</pre
|
||||
<div class="mt-1 text-xs text-primary-600/70 dark:text-primary-400/70">
|
||||
{{ selectedTool.description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parameters Form -->
|
||||
<div v-if="paramFields.length > 0" class="flex flex-col gap-3">
|
||||
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('ai.lab.toolTest.parameters') }}
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div v-for="field in paramFields" :key="field.name" class="flex flex-col gap-1">
|
||||
<label class="text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ field.name }}
|
||||
<span v-if="field.required" class="text-red-500">*</span>
|
||||
<span v-if="field.type === 'number'" class="ml-1 text-gray-400">(number)</span>
|
||||
<span v-if="field.isArray" class="ml-1 text-gray-400">({{ field.arrayItemType }}[])</span>
|
||||
</label>
|
||||
<USelectMenu
|
||||
v-if="field.enumValues"
|
||||
v-model="paramValues[field.name]"
|
||||
:items="field.enumValues"
|
||||
:placeholder="field.description"
|
||||
size="sm"
|
||||
/>
|
||||
<UInput
|
||||
v-else
|
||||
v-model="paramValues[field.name]"
|
||||
:placeholder="
|
||||
field.isArray
|
||||
? t('ai.lab.toolTest.arrayPlaceholder')
|
||||
: field.defaultValue !== undefined
|
||||
? String(field.defaultValue)
|
||||
: field.description
|
||||
"
|
||||
:type="field.type === 'number' ? 'number' : 'text'"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Execute / Cancel Button -->
|
||||
<div class="flex items-center gap-3">
|
||||
<UButton
|
||||
v-if="!isExecuting"
|
||||
color="primary"
|
||||
:disabled="!selectedToolName"
|
||||
icon="i-heroicons-play"
|
||||
@click="execute"
|
||||
>
|
||||
</template>
|
||||
{{ t('ai.lab.toolTest.execute') }}
|
||||
</UButton>
|
||||
<UButton v-else color="error" icon="i-heroicons-stop" @click="cancel">
|
||||
{{ t('ai.lab.toolTest.cancel') }}
|
||||
</UButton>
|
||||
<UIcon v-if="isExecuting" name="i-heroicons-arrow-path" class="h-4 w-4 animate-spin text-gray-400" />
|
||||
<span v-if="elapsed !== null" class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('ai.lab.toolTest.elapsed', { ms: elapsed }) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Result -->
|
||||
<div v-if="resultJson || resultError" class="flex-1 min-h-0">
|
||||
<h3 class="mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('ai.lab.toolTest.result') }}
|
||||
</h3>
|
||||
<div
|
||||
v-if="resultError"
|
||||
class="rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-900/20 dark:text-red-400"
|
||||
>
|
||||
{{ resultError }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="resultTruncated"
|
||||
class="mb-2 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-400"
|
||||
>
|
||||
{{ t('ai.lab.toolTest.truncated') }}
|
||||
</div>
|
||||
<pre
|
||||
class="max-h-[60vh] overflow-auto rounded-lg border border-gray-200 bg-gray-50 p-3 text-xs leading-relaxed text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200"
|
||||
>{{ resultJson }}</pre
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ThemeCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -67,19 +67,39 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col border-r border-gray-200 bg-white transition-all dark:border-gray-800 dark:bg-page-dark"
|
||||
:class="isCollapsed ? 'w-10' : 'w-56'"
|
||||
class="m-3 flex h-[calc(100%-1.5rem)] shrink-0 flex-col overflow-hidden rounded-lg bg-white shadow-elevated transition-all duration-300 ease-in-out dark:bg-sidebar-dark"
|
||||
:class="isCollapsed ? 'w-14' : 'w-56'"
|
||||
>
|
||||
<!-- 面板头部 -->
|
||||
<div class="flex items-center justify-between border-b border-gray-200 p-2 dark:border-gray-800">
|
||||
<div
|
||||
class="flex items-center"
|
||||
:class="
|
||||
isCollapsed
|
||||
? 'justify-center px-2 pb-2 pt-5'
|
||||
: 'justify-between border-b border-gray-200 py-2 pl-3 pr-2 dark:border-gray-800'
|
||||
"
|
||||
>
|
||||
<span v-if="!isCollapsed" class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ t('ai.sqlLab.schema.tables') }}
|
||||
</span>
|
||||
<button
|
||||
class="rounded p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-800 dark:hover:text-gray-300"
|
||||
<UButton
|
||||
v-if="!isCollapsed"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="group flex h-9 w-9 cursor-pointer items-center justify-center rounded-full hover:bg-gray-200/60 dark:hover:bg-white/[0.06]"
|
||||
@click="isCollapsed = !isCollapsed"
|
||||
>
|
||||
<UIcon :name="isCollapsed ? 'i-heroicons-chevron-right' : 'i-heroicons-chevron-left'" class="h-4 w-4" />
|
||||
<UIcon name="i-lucide-panel-right" class="size-4 scale-x-[-1] group-hover:hidden" />
|
||||
<UIcon name="i-lucide-panel-right-close" class="size-4 hidden scale-x-[-1] group-hover:block" />
|
||||
</UButton>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="group relative flex h-9 w-9 cursor-pointer items-center justify-center rounded-full hover:bg-gray-200/60 dark:hover:bg-white/[0.06]"
|
||||
@click="isCollapsed = !isCollapsed"
|
||||
>
|
||||
<UIcon name="i-lucide-panel-right-open" class="size-4 scale-x-[-1]" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -127,15 +147,15 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- 折叠时显示图标列表 -->
|
||||
<div v-else class="flex flex-1 flex-col items-center gap-2 overflow-y-auto py-2">
|
||||
<div v-else class="flex flex-1 flex-col items-center gap-1 overflow-y-auto px-2 pb-2">
|
||||
<button
|
||||
v-for="table in schema"
|
||||
:key="table.name"
|
||||
class="rounded p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-pink-500 dark:hover:bg-gray-800"
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg text-gray-600 transition-colors hover:bg-gray-200/40 hover:text-primary-600 dark:text-gray-300 dark:hover:bg-white/[0.06] dark:hover:text-primary-400"
|
||||
:title="`${getTableLabel(table.name, locale as LocaleType)} (${table.name})`"
|
||||
@click="expandTable(table.name)"
|
||||
>
|
||||
<UIcon name="i-heroicons-table-cells" class="h-4 w-4" />
|
||||
<UIcon name="i-heroicons-table-cells" class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user