agent-Specialization/static/src/components/chat/actions/ToolAction.vue

887 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<div
class="collapsible-block tool-block"
:class="{
expanded,
processing: action.tool.status === 'preparing' || action.tool.status === 'running',
completed: action.tool.status === 'completed'
}"
>
<div class="collapsible-header" @click="$emit('toggle')">
<div class="arrow"></div>
<div class="status-icon">
<span
class="tool-icon icon icon-md"
:class="getToolAnimationClass(action.tool)"
:style="iconStyle(getToolIcon(action.tool))"
aria-hidden="true"
></span>
</div>
<span class="status-text">{{ getToolStatusText(action.tool) }}</span>
<span class="tool-desc">{{ getToolDescription(action.tool) }}</span>
</div>
<div
class="collapsible-content"
:ref="el => registerCollapseContent && registerCollapseContent(collapseKey || action.tool.id || action.id || 'tool', el)"
>
<div class="content-inner">
<div v-if="shouldUseEnhancedDisplay" v-html="renderEnhancedToolResult()"></div>
<div v-else>
<pre>{{ JSON.stringify(action.tool.result || action.tool.arguments, null, 2) }}</pre>
</div>
</div>
</div>
<div
v-if="action.tool.status === 'preparing' || action.tool.status === 'running'"
class="progress-indicator"
></div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { usePersonalizationStore } from '@/stores/personalization';
defineOptions({ name: 'ToolAction' });
const props = defineProps<{
action: any;
expanded: boolean;
iconStyle: (key: string) => Record<string, string>;
getToolAnimationClass: (tool: any) => Record<string, unknown>;
getToolIcon: (tool: any) => string;
getToolStatusText: (tool: any) => string;
getToolDescription: (tool: any) => string;
formatSearchTopic: (filters: Record<string, any>) => string;
formatSearchTime: (filters: Record<string, any>) => string;
formatSearchDomains: (filters: Record<string, any>) => string;
streamingMessage: boolean;
registerCollapseContent?: (key: string, el: Element | null) => void;
collapseKey?: string;
}>();
defineEmits<{ (event: 'toggle'): void }>();
// 初始化 store
const personalizationStore = usePersonalizationStore();
const shouldUseEnhancedDisplay = computed(() => {
return personalizationStore.form.enhanced_tool_display && props.action.tool.result;
});
function escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
function renderEnhancedToolResult(): string {
const tool = props.action.tool;
const result = tool.result;
const args = tool.arguments || {};
const name = tool.name;
// 网络检索类
if (name === 'web_search') {
return renderWebSearch(result, args);
} else if (name === 'extract_webpage') {
return renderExtractWebpage(result, args);
} else if (name === 'save_webpage') {
return renderSaveWebpage(result, args);
}
// 文件编辑类
else if (name === 'create_file' || name === 'create_folder') {
return renderCreateFile(result, args);
} else if (name === 'write_file') {
return renderWriteFile(result, args);
} else if (name === 'edit_file') {
return renderEditFile(result, args);
} else if (name === 'delete_file') {
return renderDeleteFile(result, args);
} else if (name === 'rename_file') {
return renderRenameFile(result, args);
}
// 阅读聚焦类
else if (name === 'read_file') {
return renderReadFile(result, args);
} else if (name === 'vlm_analyze') {
return renderVlmAnalyze(result, args);
} else if (name === 'ocr_image') {
return renderOcrImage(result, args);
} else if (name === 'view_image') {
return renderViewImage(result, args);
}
// 终端类
else if (name === 'terminal_session') {
return renderTerminalSession(result, args);
} else if (name === 'terminal_input') {
return renderTerminalInput(result, args);
} else if (name === 'terminal_snapshot') {
return renderTerminalSnapshot(result, args);
} else if (name === 'sleep') {
return renderSleep(result, args);
}
// 终端指令类
else if (name === 'run_command') {
return renderRunCommand(result, args);
} else if (name === 'run_python') {
return renderRunPython(result, args);
}
// 记忆类
else if (name === 'update_memory') {
return renderUpdateMemory(result, args);
}
// 待办事项类
else if (name === 'todo_create') {
return renderTodoCreate(result, args);
} else if (name === 'todo_update_task') {
return renderTodoUpdate(result, args);
}
// 彩蛋类
else if (name === 'trigger_easter_egg') {
return renderEasterEgg(result, args);
}
// 默认显示 JSON
return `<pre>${escapeHtml(JSON.stringify(result || args, null, 2))}</pre>`;
}
// ===== 网络检索类 =====
function renderWebSearch(result: any, args: any): string {
const query = result.query || args.query || '';
const filters = result.filters || {};
const totalResults = result.total_results || 0;
const results = result.results || [];
let html = '<div class="tool-result-meta">';
html += `<div><strong>搜索内容:</strong>${escapeHtml(query)}</div>`;
html += `<div><strong>主题:</strong>${escapeHtml(props.formatSearchTopic(filters))}</div>`;
html += `<div><strong>时间范围:</strong>${escapeHtml(props.formatSearchTime(filters))}</div>`;
html += `<div><strong>限定网站:</strong>${escapeHtml(props.formatSearchDomains(filters))}</div>`;
html += `<div><strong>结果数量:</strong>${totalResults}</div>`;
html += '</div>';
if (results.length > 0) {
html += '<div class="search-result-list">';
results.forEach((item: any) => {
html += '<div class="search-result-item">';
html += `<div class="search-result-title">${escapeHtml(item.title || '无标题')}</div>`;
html += '<div class="search-result-url">';
if (item.url) {
html += `<a href="${escapeHtml(item.url)}" target="_blank">${escapeHtml(item.url)}</a>`;
} else {
html += '<span>无可用链接</span>';
}
html += '</div></div>';
});
html += '</div>';
} else {
html += '<div class="tool-result-empty">未返回详细的搜索结果。</div>';
}
return html;
}
function renderExtractWebpage(result: any, args: any): string {
const url = args.url || result.url || '';
const status = result.success ? '✓ 成功' : '✗ 失败';
const content = result.content || result.text || '';
let html = '<div class="tool-result-meta">';
html += `<div><strong>URL</strong>${escapeHtml(url)}</div>`;
html += `<div><strong>状态:</strong>${status}</div>`;
html += '</div>';
if (content) {
html += '<div class="tool-result-content scrollable">';
html += `<pre>${escapeHtml(content)}</pre>`;
html += '</div>';
}
return html;
}
function renderSaveWebpage(result: any, args: any): string {
const url = args.url || result.url || '';
const path = result.path || args.save_path || '';
const status = result.success ? '✓ 已保存' : '✗ 失败';
let html = '<div class="tool-result-meta">';
html += `<div><strong>URL</strong>${escapeHtml(url)}</div>`;
html += `<div><strong>保存路径:</strong>${escapeHtml(path)}</div>`;
html += `<div><strong>状态:</strong>${status}</div>`;
html += '</div>';
return html;
}
// ===== 文件编辑类 =====
function renderCreateFile(result: any, args: any): string {
const path = args.path || result.path || '';
const status = result.success ? '✓ 已创建' : '✗ 失败';
let html = '<div class="tool-result-meta">';
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
html += `<div><strong>状态:</strong>${status}</div>`;
html += '</div>';
return html;
}
function renderWriteFile(result: any, args: any): string {
const path = args.path || result.path || '';
const status = result.success ? '✓ 已写入' : '✗ 失败';
let html = '<div class="tool-result-meta">';
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
html += `<div><strong>状态:</strong>${status}</div>`;
html += '</div>';
return html;
}
function renderEditFile(result: any, args: any): string {
const path = args.path || result.path || '';
const status = result.success ? '✓ 已编辑' : '✗ 失败';
const replacements = args.replacements || [];
let html = '<div class="tool-result-meta">';
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
html += `<div><strong>状态:</strong>${status}</div>`;
html += '</div>';
if (replacements.length > 0) {
html += '<div class="tool-result-diff scrollable">';
replacements.forEach((rep: any, idx: number) => {
if (idx > 0) html += '<div class="diff-separator">⋮</div>';
const oldText = rep.old_text || rep.old || '';
const newText = rep.new_text || rep.new || '';
if (oldText) {
const oldLines = oldText.split('\n');
oldLines.forEach((line: string) => {
html += `<div class="diff-line diff-remove">- ${escapeHtml(line)}</div>`;
});
}
if (newText) {
const newLines = newText.split('\n');
newLines.forEach((line: string) => {
html += `<div class="diff-line diff-add">+ ${escapeHtml(line)}</div>`;
});
}
});
html += '</div>';
}
return html;
}
function renderDeleteFile(result: any, args: any): string {
const path = args.path || result.path || '';
const status = result.success ? '✓ 已删除' : '✗ 失败';
let html = '<div class="tool-result-meta">';
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
html += `<div><strong>状态:</strong>${status}</div>`;
html += '</div>';
return html;
}
function renderRenameFile(result: any, args: any): string {
const oldPath = args.old_path || args.source || result.old_path || '';
const newPath = args.new_path || args.destination || result.new_path || '';
const status = result.success ? '✓ 已重命名' : '✗ 失败';
let html = '<div class="tool-result-meta">';
html += `<div><strong>路径:</strong>${escapeHtml(oldPath)}</div>`;
html += `<div><strong>状态:</strong>${status}</div>`;
html += `<div><strong>重命名:</strong>${escapeHtml(oldPath)} → ${escapeHtml(newPath)}</div>`;
html += '</div>';
return html;
}
// ===== 阅读聚焦类 =====
function renderReadFile(result: any, args: any): string {
const path = args.path || result.path || '';
const status = result.success ? '✓ 成功' : '✗ 失败';
const size = result.size || result.file_size || 0;
const content = result.content || result.text || '';
let html = '<div class="tool-result-meta">';
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
html += `<div><strong>状态:</strong>${status}</div>`;
if (size > 0) {
html += `<div><strong>大小:</strong>${formatBytes(size)}</div>`;
}
html += '</div>';
if (content) {
html += '<div class="tool-result-content scrollable">';
html += `<pre>${escapeHtml(content)}</pre>`;
html += '</div>';
}
return html;
}
function renderVlmAnalyze(result: any, args: any): string {
const path = args.image_path || args.path || result.path || '';
const status = result.success ? '✓ 成功' : '✗ 失败';
const analysis = result.analysis || result.result || '';
let html = '<div class="tool-result-meta">';
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
html += `<div><strong>状态:</strong>${status}</div>`;
html += '</div>';
if (analysis) {
html += '<div class="tool-result-content scrollable">';
html += '<div class="content-label">分析结果:</div>';
html += `<pre>${escapeHtml(analysis)}</pre>`;
html += '</div>';
}
return html;
}
function renderOcrImage(result: any, args: any): string {
const path = args.image_path || args.path || result.path || '';
const status = result.success ? '✓ 成功' : '✗ 失败';
const size = result.size || result.file_size || 0;
const text = result.text || result.ocr_text || '';
const imageUrl = result.image_url || result.url || '';
let html = '<div class="tool-result-meta">';
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
html += `<div><strong>状态:</strong>${status}</div>`;
if (size > 0) {
html += `<div><strong>大小:</strong>${formatBytes(size)}</div>`;
}
html += '</div>';
if (imageUrl) {
html += `<div class="tool-result-image"><img src="${escapeHtml(imageUrl)}" alt="OCR图片" style="max-width: 100%; height: auto;" /></div>`;
}
if (text) {
html += '<div class="tool-result-content scrollable">';
html += '<div class="content-label">识别文本:</div>';
html += `<pre>${escapeHtml(text)}</pre>`;
html += '</div>';
}
return html;
}
function renderViewImage(result: any, args: any): string {
const path = args.image_path || args.path || result.path || '';
const status = result.success ? '✓ 成功' : '✗ 失败';
const size = result.size || result.file_size || 0;
const imageUrl = result.image_url || result.url || '';
let html = '<div class="tool-result-meta">';
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
html += `<div><strong>状态:</strong>${status}</div>`;
if (size > 0) {
html += `<div><strong>大小:</strong>${formatBytes(size)}</div>`;
}
html += '</div>';
if (imageUrl) {
html += `<div class="tool-result-image"><img src="${escapeHtml(imageUrl)}" alt="查看图片" style="max-width: 100%; height: auto;" /></div>`;
}
return html;
}
// ===== 终端类 =====
function renderTerminalSession(result: any, args: any): string {
const operation = args.operation || args.action || 'start';
const sessionName = args.session_name || args.name || result.session_name || '';
const status = result.success ? '✓ 成功' : '✗ 失败';
let html = '<div class="tool-result-meta">';
html += `<div><strong>操作:</strong>${escapeHtml(operation)}</div>`;
html += `<div><strong>终端名:</strong>${escapeHtml(sessionName)}</div>`;
html += `<div><strong>状态:</strong>${status}</div>`;
html += '</div>';
return html;
}
function renderTerminalInput(result: any, args: any): string {
const command = args.command || args.input || '';
const timeout = args.timeout || 30;
const output = result.output || result.result || '';
let html = '<div class="tool-result-meta">';
html += `<div><strong>指令:</strong>${escapeHtml(command)}</div>`;
html += `<div><strong>超时时间:</strong>${timeout}秒</div>`;
html += '</div>';
if (output) {
html += '<div class="tool-result-content scrollable">';
html += '<div class="content-label">输出:</div>';
html += `<pre>${escapeHtml(output)}</pre>`;
html += '</div>';
}
return html;
}
function renderTerminalSnapshot(result: any, args: any): string {
const sessionName = args.session_name || args.name || result.session_name || '';
const status = result.success ? '✓ 成功' : '✗ 失败';
const startLine = args.start_line || 0;
const endLine = args.end_line || 0;
const content = result.content || result.output || '';
let html = '<div class="tool-result-meta">';
html += `<div><strong>状态:</strong>${status}</div>`;
html += `<div><strong>终端名:</strong>${escapeHtml(sessionName)}</div>`;
if (startLine || endLine) {
html += `<div><strong>行范围:</strong>${startLine} - ${endLine}</div>`;
}
html += '</div>';
if (content) {
html += '<div class="tool-result-content scrollable">';
html += '<div class="content-label">获取的内容:</div>';
html += `<pre>${escapeHtml(content)}</pre>`;
html += '</div>';
}
return html;
}
function renderSleep(result: any, args: any): string {
const seconds = args.seconds || args.duration || 0;
const status = result.success ? '✓ 完成' : '✗ 失败';
let html = '<div class="tool-result-meta">';
html += `<div><strong>等待时间:</strong>${seconds}秒</div>`;
html += `<div><strong>状态:</strong>${status}</div>`;
html += '</div>';
return html;
}
// ===== 终端指令类 =====
function renderRunCommand(result: any, args: any): string {
const command = args.command || '';
const timeout = args.timeout || 30;
const output = result.output || result.stdout || '';
const exitCode = result.exit_code !== undefined ? result.exit_code : result.returncode;
let html = '<div class="tool-result-meta">';
html += `<div><strong>指令:</strong>${escapeHtml(command)}</div>`;
html += `<div><strong>超时时间:</strong>${timeout}秒</div>`;
if (exitCode !== undefined) {
html += `<div><strong>退出码:</strong>${exitCode}</div>`;
}
html += '</div>';
if (output) {
html += '<div class="tool-result-content scrollable">';
html += '<div class="content-label">输出:</div>';
html += `<pre>${escapeHtml(output)}</pre>`;
html += '</div>';
}
return html;
}
function renderRunPython(result: any, args: any): string {
const code = result.code || args.code || '';
const output = result.output || '';
let html = '<div class="code-block">';
html += '<div class="code-label">代码:</div>';
html += `<pre><code class="language-python">${escapeHtml(code)}</code></pre>`;
html += '</div>';
if (output) {
html += '<div class="output-block">';
html += '<div class="output-label">输出:</div>';
html += `<pre>${escapeHtml(output)}</pre>`;
html += '</div>';
}
return html;
}
// ===== 记忆类 =====
function renderUpdateMemory(result: any, args: any): string {
const operations = args.operations || [];
const added = operations.filter((op: any) => op.action === 'add' || op.action === 'create');
const updated = operations.filter((op: any) => op.action === 'update' || op.action === 'modify');
const deleted = operations.filter((op: any) => op.action === 'delete' || op.action === 'remove');
let html = '<div class="tool-result-meta">';
html += `<div><strong>操作:</strong>添加 ${added.length} 条 | 更新 ${updated.length} 条 | 删除 ${deleted.length} 条</div>`;
html += '</div>';
html += '<div class="tool-result-content scrollable">';
if (added.length > 0) {
html += '<div class="memory-section">';
added.forEach((op: any) => {
html += `<div class="memory-item memory-add">+ ${escapeHtml(op.content || op.text || '')}</div>`;
});
html += '</div>';
}
if (updated.length > 0) {
html += '<div class="memory-section">';
updated.forEach((op: any) => {
html += `<div class="memory-item memory-update">✏️ ${escapeHtml(op.content || op.text || '')}</div>`;
});
html += '</div>';
}
if (deleted.length > 0) {
html += '<div class="memory-section">';
deleted.forEach((op: any) => {
html += `<div class="memory-item memory-delete">- ${escapeHtml(op.content || op.text || '')}</div>`;
});
html += '</div>';
}
html += '</div>';
return html;
}
// ===== 待办事项类 =====
function renderTodoCreate(result: any, args: any): string {
const status = result.success ? '✓ 已创建' : '✗ 失败';
const title = args.title || result.title || '';
const tasks = args.tasks || result.tasks || [];
let html = '<div class="tool-result-meta">';
html += `<div><strong>状态:</strong>${status}</div>`;
html += `<div><strong>标题:</strong>${escapeHtml(title)}</div>`;
html += '</div>';
if (tasks.length > 0) {
html += '<div class="tool-result-content">';
html += '<div class="todo-list">';
tasks.forEach((task: any) => {
const taskText = typeof task === 'string' ? task : (task.text || task.title || '');
html += `<div class="todo-item"><input type="checkbox" disabled /> ${escapeHtml(taskText)}</div>`;
});
html += '</div>';
html += '</div>';
}
return html;
}
function renderTodoUpdate(result: any, args: any): string {
const status = result.success ? '✓ 已更新' : '✗ 失败';
const title = args.title || result.title || '';
const tasks = result.tasks || args.tasks || [];
let html = '<div class="tool-result-meta">';
html += `<div><strong>状态:</strong>${status}</div>`;
html += `<div><strong>标题:</strong>${escapeHtml(title)}</div>`;
html += '</div>';
if (tasks.length > 0) {
html += '<div class="tool-result-content">';
html += '<div class="todo-list">';
tasks.forEach((task: any) => {
const taskText = typeof task === 'string' ? task : (task.text || task.title || '');
const completed = typeof task === 'object' && (task.completed || task.done || task.checked);
html += `<div class="todo-item"><input type="checkbox" ${completed ? 'checked' : ''} disabled /> ${escapeHtml(taskText)}</div>`;
});
html += '</div>';
html += '</div>';
}
return html;
}
// ===== 彩蛋类 =====
function renderEasterEgg(result: any, args: any): string {
const status = result.success ? '✓ 已触发' : '✗ 失败';
const type = result.type || args.type || '';
const content = result.content || result.message || '';
let html = '<div class="tool-result-meta">';
html += `<div><strong>状态:</strong>${status}</div>`;
html += `<div><strong>类型:</strong>${escapeHtml(type)}</div>`;
html += '</div>';
if (content) {
html += '<div class="tool-result-content">';
html += `<div class="easter-egg-content">${escapeHtml(content)}</div>`;
html += '</div>';
}
return html;
}
</script>
<style>
/* 工具增强显示样式 - 不使用 scoped 以便应用到 v-html 内容 */
.tool-result-meta {
margin-bottom: 12px;
padding: 8px 12px;
background: rgba(0, 0, 0, 0.02);
border-radius: 6px;
font-size: 13px;
line-height: 1.6;
}
.tool-result-meta > div {
margin: 4px 0;
}
.tool-result-content {
margin-top: 12px;
}
.tool-result-content.scrollable {
max-height: 400px;
overflow-y: auto;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 6px;
padding: 8px;
background: rgba(0, 0, 0, 0.01);
}
.tool-result-content pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 12px;
line-height: 1.5;
}
.content-label {
font-weight: 600;
margin-bottom: 8px;
font-size: 13px;
}
.tool-result-empty {
color: rgba(0, 0, 0, 0.5);
font-style: italic;
padding: 8px;
}
/* 搜索结果 */
.search-result-list {
margin-top: 12px;
}
.search-result-item {
padding: 10px;
margin-bottom: 8px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 6px;
background: rgba(0, 0, 0, 0.01);
}
/* 暗色模式下的搜索结果 */
:root[data-theme='dark'] .search-result-item {
background: #2a2a2a;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.search-result-title {
font-weight: 600;
margin-bottom: 4px;
font-size: 14px;
}
.search-result-url {
font-size: 12px;
color: rgba(0, 0, 0, 0.6);
}
:root[data-theme='dark'] .search-result-url {
color: rgba(255, 255, 255, 0.6);
}
.search-result-url a {
color: #0066cc;
text-decoration: none;
}
:root[data-theme='dark'] .search-result-url a {
color: #6b9fff;
}
.search-result-url a:hover {
text-decoration: underline;
}
/* 文件差异 */
.tool-result-diff {
margin-top: 12px;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 12px;
line-height: 1.5;
}
.tool-result-diff.scrollable {
max-height: 400px;
overflow-y: auto;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 6px;
padding: 8px;
background: rgba(0, 0, 0, 0.01);
}
.diff-line {
padding: 2px 4px;
margin: 1px 0;
white-space: pre-wrap;
word-wrap: break-word;
}
.diff-remove {
background: rgba(255, 0, 0, 0.1);
color: #c00;
}
.diff-add {
background: rgba(0, 255, 0, 0.1);
color: #0a0;
}
.diff-separator {
text-align: center;
color: rgba(0, 0, 0, 0.3);
margin: 8px 0;
}
.diff-operation {
font-weight: 600;
margin: 8px 0 4px;
color: rgba(0, 0, 0, 0.7);
}
/* 图片 */
.tool-result-image {
margin-top: 12px;
text-align: center;
}
.tool-result-image img {
max-width: 100%;
height: auto;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, 0.1);
}
/* 代码块 */
.code-block {
margin-top: 12px;
}
.code-label,
.output-label {
font-weight: 600;
margin-bottom: 6px;
font-size: 13px;
}
.code-block pre,
.output-block pre {
margin: 0;
padding: 12px;
background: rgba(0, 0, 0, 0.03);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 6px;
overflow-x: auto;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
}
.output-block {
margin-top: 12px;
}
/* 记忆 */
.memory-section {
margin-bottom: 12px;
}
.memory-item {
padding: 6px 8px;
margin: 4px 0;
border-radius: 4px;
font-size: 13px;
line-height: 1.5;
}
.memory-add {
background: rgba(0, 255, 0, 0.1);
color: #0a0;
}
.memory-update {
background: rgba(255, 165, 0, 0.1);
color: #f90;
}
.memory-delete {
background: rgba(255, 0, 0, 0.1);
color: #c00;
}
/* 待办事项 */
.todo-list {
padding: 8px;
}
.todo-item {
padding: 6px 0;
font-size: 13px;
line-height: 1.5;
display: flex;
align-items: center;
gap: 8px;
}
.todo-item input[type="checkbox"] {
margin: 0;
}
/* 彩蛋 */
.easter-egg-content {
padding: 12px;
background: linear-gradient(135deg, rgba(255, 215, 0, 0.1), rgba(255, 105, 180, 0.1));
border: 1px solid rgba(255, 215, 0, 0.3);
border-radius: 8px;
font-size: 14px;
line-height: 1.6;
}
</style>