feat(gui): refactor chat panel with Markdown rendering and tool indicators (U3)
- ChatMessage: replace v-html with markdown-it, add ToolCallIndicator - ChatInput: add ContextPill support for file/skill context - ChatSidebar: make collapsible (default collapsed) - Create ToolCallIndicator.vue with color-coded tool type badges - Create ContextPill.vue for context references - Replace all hardcoded colors with Design Token references - Install markdown-it for Markdown rendering
This commit is contained in:
parent
9273612a5b
commit
6d5a08cb0c
File diff suppressed because it is too large
Load Diff
|
|
@ -14,11 +14,13 @@
|
|||
"@vue-flow/controls": "^1.1.0",
|
||||
"@vue-flow/core": "^1.41.0",
|
||||
"ant-design-vue": "^4.2.0",
|
||||
"markdown-it": "^14.2.0",
|
||||
"pinia": "^2.2.0",
|
||||
"vue": "^3.5.0",
|
||||
"vue-router": "^4.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@vitejs/plugin-vue": "^5.1.0",
|
||||
"echarts": "^6.1.0",
|
||||
"typescript": "^5.6.0",
|
||||
|
|
|
|||
|
|
@ -1,33 +1,51 @@
|
|||
<template>
|
||||
<div class="chat-input">
|
||||
<a-textarea
|
||||
v-model:value="inputText"
|
||||
:placeholder="placeholder"
|
||||
:auto-size="{ minRows: 1, maxRows: 4 }"
|
||||
:disabled="disabled"
|
||||
@pressEnter="handlePressEnter"
|
||||
class="chat-input__textarea"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="!canSend"
|
||||
:loading="disabled"
|
||||
@click="handleSend"
|
||||
class="chat-input__send"
|
||||
>
|
||||
<template #icon><SendOutlined /></template>
|
||||
发送
|
||||
</a-button>
|
||||
<div v-if="contextPills.length > 0" class="chat-input__pills">
|
||||
<ContextPill
|
||||
v-for="(pill, idx) in contextPills"
|
||||
:key="idx"
|
||||
:label="pill.label"
|
||||
:icon="pill.icon"
|
||||
:removable="pill.removable"
|
||||
@remove="removePill(idx)"
|
||||
/>
|
||||
</div>
|
||||
<div class="chat-input__row">
|
||||
<a-textarea
|
||||
v-model:value="inputText"
|
||||
:placeholder="placeholder"
|
||||
:auto-size="{ minRows: 1, maxRows: 4 }"
|
||||
:disabled="disabled"
|
||||
@pressEnter="handlePressEnter"
|
||||
class="chat-input__textarea"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="!canSend"
|
||||
:loading="disabled"
|
||||
@click="handleSend"
|
||||
class="chat-input__send"
|
||||
>
|
||||
<template #icon><SendOutlined /></template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, type Component } from 'vue'
|
||||
import { Input as AInput, Button as AButton } from 'ant-design-vue'
|
||||
import { SendOutlined } from '@ant-design/icons-vue'
|
||||
import ContextPill from './ContextPill.vue'
|
||||
|
||||
const ATextarea = AInput.TextArea
|
||||
|
||||
interface ContextPillData {
|
||||
label: string
|
||||
icon?: Component
|
||||
removable?: boolean
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
disabled?: boolean
|
||||
placeholder?: string
|
||||
|
|
@ -43,6 +61,7 @@ const emit = defineEmits<{
|
|||
}>()
|
||||
|
||||
const inputText = ref('')
|
||||
const contextPills = ref<ContextPillData[]>([])
|
||||
|
||||
const canSend = computed(() => {
|
||||
return inputText.value.trim().length > 0 && !props.disabled
|
||||
|
|
@ -56,20 +75,36 @@ function handleSend(): void {
|
|||
}
|
||||
|
||||
function handlePressEnter(event: KeyboardEvent): void {
|
||||
if (event.shiftKey) return // Shift+Enter for new line
|
||||
if (event.shiftKey) return
|
||||
event.preventDefault()
|
||||
handleSend()
|
||||
}
|
||||
|
||||
function removePill(idx: number): void {
|
||||
contextPills.value.splice(idx, 1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--bg-primary);
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.chat-input__pills {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
flex-wrap: wrap;
|
||||
padding-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.chat-input__row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.chat-input__textarea {
|
||||
|
|
|
|||
|
|
@ -3,26 +3,38 @@
|
|||
<div class="chat-message__avatar">
|
||||
<a-avatar
|
||||
v-if="message.role === 'assistant'"
|
||||
:size="36"
|
||||
style="background-color: #1677ff"
|
||||
:size="32"
|
||||
class="chat-message__avatar--assistant"
|
||||
>
|
||||
<template #icon><RobotOutlined /></template>
|
||||
</a-avatar>
|
||||
<a-avatar
|
||||
v-else
|
||||
:size="36"
|
||||
style="background-color: #52c41a"
|
||||
:size="32"
|
||||
class="chat-message__avatar--user"
|
||||
>
|
||||
<template #icon><UserOutlined /></template>
|
||||
</a-avatar>
|
||||
</div>
|
||||
<div class="chat-message__body">
|
||||
<!-- Tool call indicators -->
|
||||
<div v-if="toolCalls.length > 0" class="chat-message__tools">
|
||||
<ToolCallIndicator
|
||||
v-for="(tc, idx) in toolCalls"
|
||||
:key="idx"
|
||||
:type="tc.type"
|
||||
:name="tc.name"
|
||||
/>
|
||||
</div>
|
||||
<!-- Message content -->
|
||||
<div class="chat-message__content" :class="[`chat-message__content--${message.role}`]">
|
||||
<span v-html="renderedContent"></span>
|
||||
<div v-if="message.role === 'assistant'" class="chat-message__markdown" v-html="renderedContent"></div>
|
||||
<span v-else>{{ message.content }}</span>
|
||||
<a-spin v-if="isLoading" size="small" class="chat-message__loading" />
|
||||
</div>
|
||||
<!-- Routing info -->
|
||||
<div v-if="showRouting" class="chat-message__routing">
|
||||
<a-tag color="blue">
|
||||
<a-tag color="purple">
|
||||
<ThunderboltOutlined /> {{ message.matched_skill }}
|
||||
</a-tag>
|
||||
<a-tag v-if="message.confidence !== undefined" color="green">
|
||||
|
|
@ -41,9 +53,22 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { Avatar as AAvatar, Tag as ATag, Spin as ASpin } from 'ant-design-vue'
|
||||
import { RobotOutlined, UserOutlined, ThunderboltOutlined } from '@ant-design/icons-vue'
|
||||
import type { IChatMessage } from '@/api/types'
|
||||
import ToolCallIndicator from './ToolCallIndicator.vue'
|
||||
|
||||
const md = new MarkdownIt({
|
||||
html: false,
|
||||
linkify: true,
|
||||
breaks: true,
|
||||
})
|
||||
|
||||
interface ToolCall {
|
||||
type: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
message: IChatMessage
|
||||
|
|
@ -65,20 +90,32 @@ const formattedTime = computed(() => {
|
|||
})
|
||||
|
||||
const renderedContent = computed(() => {
|
||||
// Simple rendering: escape HTML and convert newlines
|
||||
const escaped = props.message.content
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
return escaped.replace(/\n/g, '<br/>')
|
||||
if (!props.message.content) return ''
|
||||
return md.render(props.message.content)
|
||||
})
|
||||
|
||||
const toolCalls = computed<ToolCall[]>(() => {
|
||||
// Extract tool calls from message metadata or content patterns
|
||||
const calls: ToolCall[] = []
|
||||
const content = props.message.content || ''
|
||||
|
||||
// Detect tool use patterns like [Read], [Edit], [Bash], [Write], [Search]
|
||||
const toolPattern = /\[(Read|Edit|Bash|Write|Search|Grep|Glob)\]/g
|
||||
let match
|
||||
while ((match = toolPattern.exec(content)) !== null) {
|
||||
const toolName = match[1].toLowerCase()
|
||||
calls.push({ type: toolName, name: match[1] })
|
||||
}
|
||||
|
||||
return calls
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-message {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
}
|
||||
|
||||
.chat-message--user {
|
||||
|
|
@ -89,50 +126,111 @@ const renderedContent = computed(() => {
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-message__avatar--assistant {
|
||||
background: var(--gradient-brand) !important;
|
||||
}
|
||||
|
||||
.chat-message__avatar--user {
|
||||
background-color: var(--color-success) !important;
|
||||
}
|
||||
|
||||
.chat-message__body {
|
||||
max-width: 70%;
|
||||
max-width: 75%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.chat-message--user .chat-message__body {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.chat-message__tools {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chat-message__content {
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-radius: var(--radius-lg);
|
||||
line-height: var(--leading-normal);
|
||||
font-size: var(--font-base);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.chat-message__content--user {
|
||||
background: #1677ff;
|
||||
color: #ffffff;
|
||||
border-bottom-right-radius: 4px;
|
||||
background: var(--color-primary);
|
||||
color: var(--text-inverse);
|
||||
border-bottom-right-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.chat-message__content--assistant {
|
||||
background: #ffffff;
|
||||
color: #333333;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-bottom-left-radius: 4px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-bottom-left-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.chat-message__markdown {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.chat-message__markdown :deep(p) {
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.chat-message__markdown :deep(p:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.chat-message__markdown :deep(pre) {
|
||||
background: var(--code-bg);
|
||||
color: var(--code-fg);
|
||||
padding: var(--space-3);
|
||||
border-radius: var(--radius-md);
|
||||
overflow-x: auto;
|
||||
margin: var(--space-2) 0;
|
||||
font-size: var(--font-sm);
|
||||
}
|
||||
|
||||
.chat-message__markdown :deep(code) {
|
||||
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace;
|
||||
font-size: var(--font-sm);
|
||||
}
|
||||
|
||||
.chat-message__markdown :deep(:not(pre) > code) {
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
padding: 1px var(--space-1);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.chat-message__markdown :deep(ul),
|
||||
.chat-message__markdown :deep(ol) {
|
||||
padding-left: var(--space-5);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.chat-message__markdown :deep(h1),
|
||||
.chat-message__markdown :deep(h2),
|
||||
.chat-message__markdown :deep(h3) {
|
||||
margin-top: var(--space-3);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.chat-message__loading {
|
||||
margin-left: 8px;
|
||||
margin-left: var(--space-2);
|
||||
}
|
||||
|
||||
.chat-message__routing {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
gap: var(--space-1);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chat-message__time {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
font-size: var(--font-xs);
|
||||
color: var(--text-placeholder);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,31 +1,38 @@
|
|||
<template>
|
||||
<div class="chat-sidebar">
|
||||
<div class="chat-sidebar__header">
|
||||
<a-button type="primary" block @click="handleCreate">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建对话
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="chat-sidebar__list">
|
||||
<a-empty v-if="conversations.length === 0" description="暂无对话" />
|
||||
<div
|
||||
v-for="conv in conversations"
|
||||
:key="conv.id"
|
||||
class="chat-sidebar__item"
|
||||
:class="{ 'chat-sidebar__item--active': conv.id === currentId }"
|
||||
@click="handleSelect(conv.id)"
|
||||
>
|
||||
<MessageOutlined class="chat-sidebar__item-icon" />
|
||||
<span class="chat-sidebar__item-title">{{ conv.title }}</span>
|
||||
<span class="chat-sidebar__item-time">{{ formatRelativeTime(conv.updated_at) }}</span>
|
||||
<div :class="['chat-sidebar', { 'chat-sidebar--collapsed': collapsed }]">
|
||||
<div v-if="!collapsed" class="chat-sidebar__content">
|
||||
<div class="chat-sidebar__header">
|
||||
<a-button type="primary" block size="small" @click="handleCreate">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建对话
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="chat-sidebar__list">
|
||||
<a-empty v-if="conversations.length === 0" description="暂无对话" :image-style="{ height: '40px' }" />
|
||||
<div
|
||||
v-for="conv in conversations"
|
||||
:key="conv.id"
|
||||
class="chat-sidebar__item"
|
||||
:class="{ 'chat-sidebar__item--active': conv.id === currentId }"
|
||||
@click="handleSelect(conv.id)"
|
||||
>
|
||||
<MessageOutlined class="chat-sidebar__item-icon" />
|
||||
<span class="chat-sidebar__item-title">{{ conv.title }}</span>
|
||||
<span class="chat-sidebar__item-time">{{ formatRelativeTime(conv.updated_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="chat-sidebar__toggle" @click="collapsed = !collapsed" :title="collapsed ? '展开' : '折叠'">
|
||||
<RightOutlined v-if="collapsed" />
|
||||
<LeftOutlined v-else />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Button as AButton, Empty as AEmpty } from 'ant-design-vue'
|
||||
import { PlusOutlined, MessageOutlined } from '@ant-design/icons-vue'
|
||||
import { PlusOutlined, MessageOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons-vue'
|
||||
import type { IConversation } from '@/api/types'
|
||||
|
||||
interface IProps {
|
||||
|
|
@ -40,6 +47,8 @@ const emit = defineEmits<{
|
|||
select: [id: string]
|
||||
}>()
|
||||
|
||||
const collapsed = ref(true)
|
||||
|
||||
function handleCreate(): void {
|
||||
emit('create')
|
||||
}
|
||||
|
|
@ -69,69 +78,102 @@ function formatRelativeTime(dateStr: string): string {
|
|||
|
||||
<style scoped>
|
||||
.chat-sidebar {
|
||||
width: 280px;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
background: #ffffff;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
background: var(--bg-primary);
|
||||
border-right: 1px solid var(--border-color);
|
||||
transition: width var(--transition-normal);
|
||||
}
|
||||
|
||||
.chat-sidebar--collapsed {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.chat-sidebar:not(.chat-sidebar--collapsed) {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.chat-sidebar__content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-sidebar__header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: var(--space-3);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.chat-sidebar__list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
padding: var(--space-2);
|
||||
}
|
||||
|
||||
.chat-sidebar__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
|
||||
.chat-sidebar__item:hover {
|
||||
background: #f5f5f5;
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.chat-sidebar__item--active {
|
||||
background: #e6f4ff;
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
|
||||
.chat-sidebar__item--active:hover {
|
||||
background: #e6f4ff;
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
|
||||
.chat-sidebar__item-icon {
|
||||
color: #999999;
|
||||
font-size: 14px;
|
||||
color: var(--text-placeholder);
|
||||
font-size: var(--font-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-sidebar__item--active .chat-sidebar__item-icon {
|
||||
color: #1677ff;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.chat-sidebar__item-title {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
font-size: var(--font-sm);
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-sidebar__item-time {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
font-size: var(--font-xs);
|
||||
color: var(--text-placeholder);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-sidebar__toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.chat-sidebar__toggle:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<span class="context-pill">
|
||||
<component :is="icon" class="context-pill__icon" />
|
||||
<span class="context-pill__label">{{ label }}</span>
|
||||
<button v-if="removable" class="context-pill__remove" @click.stop="$emit('remove')">
|
||||
<CloseOutlined />
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type Component } from 'vue'
|
||||
import { CloseOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
defineProps<{
|
||||
label: string
|
||||
icon?: Component
|
||||
removable?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
remove: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.context-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
padding: 2px var(--space-2);
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--font-xs);
|
||||
color: var(--text-secondary);
|
||||
max-width: 160px;
|
||||
}
|
||||
|
||||
.context-pill__icon {
|
||||
font-size: 10px;
|
||||
color: var(--text-tertiary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.context-pill__label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.context-pill__remove {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-placeholder);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-full);
|
||||
flex-shrink: 0;
|
||||
padding: 0;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.context-pill__remove:hover {
|
||||
color: var(--color-error);
|
||||
background: var(--color-error-light);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<span :class="['tool-call-indicator', `tool-call-indicator--${type}`]">
|
||||
<component :is="icon" class="tool-call-indicator__icon" />
|
||||
<span class="tool-call-indicator__label">{{ name }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, type Component } from 'vue'
|
||||
import {
|
||||
ReadOutlined,
|
||||
EditOutlined,
|
||||
CodeOutlined,
|
||||
FileAddOutlined,
|
||||
SearchOutlined,
|
||||
FolderOpenOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const props = defineProps<{
|
||||
type: string
|
||||
name: string
|
||||
}>()
|
||||
|
||||
const iconMap: Record<string, Component> = {
|
||||
read: ReadOutlined,
|
||||
edit: EditOutlined,
|
||||
bash: CodeOutlined,
|
||||
write: FileAddOutlined,
|
||||
search: SearchOutlined,
|
||||
grep: SearchOutlined,
|
||||
glob: FolderOpenOutlined,
|
||||
}
|
||||
|
||||
const icon = computed(() => iconMap[props.type] || CodeOutlined)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tool-call-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 1px var(--space-2);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 11px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.tool-call-indicator__icon {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.tool-call-indicator--read {
|
||||
background: var(--color-info-light);
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
.tool-call-indicator--edit {
|
||||
background: var(--color-warning-light);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.tool-call-indicator--bash {
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.tool-call-indicator--write {
|
||||
background: var(--color-success-light);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.tool-call-indicator--search,
|
||||
.tool-call-indicator--grep,
|
||||
.tool-call-indicator--glob {
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -119,7 +119,7 @@ function handleSend(message: string): void {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #f5f5f5;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.chat-view__empty {
|
||||
|
|
@ -132,7 +132,7 @@ function handleSend(message: string): void {
|
|||
.chat-view__messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px 0;
|
||||
padding: var(--space-4) 0;
|
||||
}
|
||||
|
||||
.chat-view__welcome {
|
||||
|
|
@ -141,45 +141,48 @@ function handleSend(message: string): void {
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #999999;
|
||||
color: var(--text-placeholder);
|
||||
}
|
||||
|
||||
.chat-view__welcome-icon {
|
||||
font-size: 48px;
|
||||
color: #1677ff;
|
||||
margin-bottom: 16px;
|
||||
background: var(--gradient-brand);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.chat-view__welcome h2 {
|
||||
color: #333333;
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-xl);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.chat-view__welcome p {
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
font-size: var(--font-base);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.chat-view__steps {
|
||||
padding: 8px 16px;
|
||||
margin: 0 16px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #f0f0f0;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
margin: 0 var(--space-4);
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.chat-view__step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: var(--space-1);
|
||||
padding: 2px 0;
|
||||
font-size: 13px;
|
||||
color: #666666;
|
||||
font-size: var(--font-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.chat-view__step-icon {
|
||||
font-size: 10px;
|
||||
color: #1677ff;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue