Skip to content

Commit 34f74af

Browse files
committed
feat(gui): enhance chat view and desktop integration
1 parent ed278d5 commit 34f74af

4 files changed

Lines changed: 92 additions & 10 deletions

File tree

agent-diva-gui/src/App.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ interface Message {
3232
toolCallId?: string;
3333
rawMeta?: Record<string, unknown>;
3434
fromHistory?: boolean;
35-
attachments?: string[];
35+
attachments?: FileAttachmentDto[];
3636
}
3737
3838
interface ToolStartPayload {
@@ -98,6 +98,7 @@ interface BackendChatMessage {
9898
tool_calls?: serdeJsonValue[] | null;
9999
name?: string | null;
100100
thinking_blocks?: serdeJsonValue[] | null;
101+
attachments?: FileAttachmentDto[] | null;
101102
}
102103
103104
interface BackendSessionHistory {
@@ -446,6 +447,7 @@ function mapBackendMessageToUi(msg: BackendChatMessage): Message | null {
446447
toolCallId: msg.tool_call_id || undefined,
447448
rawMeta,
448449
fromHistory: true,
450+
attachments: msg.attachments ?? undefined,
449451
};
450452
}
451453
@@ -591,7 +593,7 @@ async function sendMessage(content: string, attachments?: FileAttachmentDto[]) {
591593
role: 'user',
592594
content: content,
593595
timestamp: Date.now(),
594-
attachments: attachmentFileIds
596+
attachments: attachments ?? []
595597
};
596598
messages.value.push(userMsg);
597599

agent-diva-gui/src/api/desktop.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ export interface FileAttachmentDto {
2222
filename: string;
2323
size: number;
2424
mime_type?: string | null;
25-
channel: string;
25+
channel?: string;
2626
message_id?: string | null;
2727
uploaded_by?: string | null;
28-
stored_at: string;
29-
ref_count: number;
28+
stored_at?: string;
29+
ref_count?: number;
3030
}
3131

3232
export interface McpConnectionStatusDto {

agent-diva-gui/src/components/ChatView.vue

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
22
import { ref, computed, nextTick, watch, onMounted } from 'vue';
33
import { invoke } from '@tauri-apps/api/core';
4-
import { Send, Square, Plus, Wrench, ChevronDown, ChevronRight, CheckCircle2, XCircle, Loader2, Brain, Paperclip, X } from 'lucide-vue-next';
4+
import { Send, Square, Plus, Wrench, ChevronDown, ChevronRight, CheckCircle2, XCircle, Loader2, Brain, Paperclip, X, Image as ImageIcon, File as FileIcon } from 'lucide-vue-next';
55
import MarkdownIt from 'markdown-it';
66
import hljs from 'highlight.js';
77
import 'highlight.js/styles/github-dark.css'; // 使用 GitHub Dark 风格
@@ -50,6 +50,7 @@ interface Message {
5050
toolCallId?: string;
5151
rawMeta?: Record<string, unknown>;
5252
fromHistory?: boolean;
53+
attachments?: FileAttachmentDto[];
5354
}
5455
5556
const expandedTools = ref<Record<number, boolean>>({});
@@ -98,6 +99,7 @@ const props = defineProps<{
9899
isTyping: boolean;
99100
themeMode?: string;
100101
historyPrefs?: HistoryPrefs;
102+
currentModel?: string;
101103
}>();
102104
103105
const emit = defineEmits<{
@@ -112,6 +114,43 @@ const inputRef = ref<HTMLTextAreaElement | null>(null);
112114
const fileInputRef = ref<HTMLInputElement | null>(null);
113115
const attachments = ref<FileAttachmentDto[]>([]);
114116
const uploading = ref(false);
117+
const uploadError = ref<string | null>(null);
118+
119+
const visionModels = new Set(['gpt-4o', 'gpt-4o-mini', 'gpt-4.1', 'gpt-4.1-mini']);
120+
121+
const normalizeModelId = (model?: string) => {
122+
const trimmed = (model || '').trim().toLowerCase();
123+
const parts = trimmed.split('/');
124+
return parts[parts.length - 1] || trimmed;
125+
};
126+
127+
const supportsVisionModel = (model?: string) => visionModels.has(normalizeModelId(model));
128+
129+
const isImageAttachment = (attachment: FileAttachmentDto) =>
130+
attachment.mime_type?.toLowerCase().startsWith('image/') ?? false;
131+
132+
const hasImageAttachments = computed(() => attachments.value.some(isImageAttachment));
133+
134+
const showVisionWarning = computed(
135+
() => hasImageAttachments.value && !supportsVisionModel(props.currentModel)
136+
);
137+
138+
const formatFileSize = (size: number) => {
139+
if (!Number.isFinite(size) || size < 0) return '';
140+
if (size < 1024) return `${size} B`;
141+
const kb = size / 1024;
142+
if (kb < 1024) return `${kb.toFixed(kb >= 10 ? 0 : 1)} KB`;
143+
const mb = kb / 1024;
144+
return `${mb.toFixed(mb >= 10 ? 0 : 1)} MB`;
145+
};
146+
147+
const attachmentLabel = (attachment: FileAttachmentDto) =>
148+
isImageAttachment(attachment) ? '图片' : '文件';
149+
150+
const attachmentTypeText = (attachment: FileAttachmentDto) => {
151+
const size = formatFileSize(attachment.size);
152+
return [attachmentLabel(attachment), size].filter(Boolean).join(' · ');
153+
};
115154
116155
const effectiveHistoryPrefs = computed<HistoryPrefs>(() => ({
117156
...defaultHistoryPrefs,
@@ -168,6 +207,7 @@ const handleFileSelect = async (event: Event) => {
168207
if (!files || files.length === 0) return;
169208
170209
uploading.value = true;
210+
uploadError.value = null;
171211
try {
172212
for (const file of files) {
173213
const bytes = await file.arrayBuffer();
@@ -177,6 +217,7 @@ const handleFileSelect = async (event: Event) => {
177217
}
178218
} catch (error) {
179219
console.error('Failed to upload file:', error);
220+
uploadError.value = `文件上传失败:${error instanceof Error ? error.message : String(error)}`;
180221
} finally {
181222
uploading.value = false;
182223
if (fileInputRef.value) {
@@ -187,6 +228,9 @@ const handleFileSelect = async (event: Event) => {
187228
188229
const handleRemoveAttachment = (index: number) => {
189230
attachments.value.splice(index, 1);
231+
if (attachments.value.length === 0) {
232+
uploadError.value = null;
233+
}
190234
};
191235
192236
const handleSend = () => {
@@ -199,6 +243,7 @@ const handleSend = () => {
199243
emit('send', message, currentAttachments);
200244
input.value = '';
201245
attachments.value = [];
246+
uploadError.value = null;
202247
};
203248
204249
const handleClear = async () => {
@@ -433,6 +478,23 @@ const getEmotionEmoji = (emotion?: string) => {
433478
<div class="w-1.5 h-1.5 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.2s" />
434479
</div>
435480
<div v-else class="markdown-body" v-html="md.render(msg.content)"></div>
481+
<div
482+
v-if="msg.attachments && msg.attachments.length > 0"
483+
class="mt-2 flex flex-wrap gap-1.5"
484+
>
485+
<div
486+
v-for="attachment in msg.attachments"
487+
:key="attachment.file_id"
488+
class="flex max-w-full items-center gap-1.5 rounded-md border px-2 py-1 text-[11px]"
489+
:class="isImageAttachment(attachment) ? 'border-pink-200 bg-pink-50/80 text-pink-700' : 'border-gray-200 bg-white/70 text-gray-600'"
490+
:title="`${attachment.filename} (${attachmentTypeText(attachment)})`"
491+
>
492+
<ImageIcon v-if="isImageAttachment(attachment)" :size="13" class="shrink-0" />
493+
<FileIcon v-else :size="13" class="shrink-0" />
494+
<span class="max-w-[160px] truncate font-medium">{{ attachment.filename }}</span>
495+
<span class="shrink-0 opacity-70">{{ formatFileSize(attachment.size) }}</span>
496+
</div>
497+
</div>
436498
</div>
437499

438500
<!-- Timestamp -->
@@ -466,18 +528,35 @@ const getEmotionEmoji = (emotion?: string) => {
466528
<div
467529
v-for="(attachment, index) in attachments"
468530
:key="attachment.file_id"
469-
class="flex items-center gap-1 bg-gray-100 rounded-lg px-2 py-1 text-xs"
531+
class="flex max-w-full items-center gap-1.5 rounded-lg border px-2 py-1 text-xs"
532+
:class="isImageAttachment(attachment) ? 'border-pink-200 bg-pink-50 text-pink-700' : 'border-gray-200 bg-gray-100 text-gray-700'"
533+
:title="`${attachment.filename} (${attachmentTypeText(attachment)})`"
470534
>
471-
<Paperclip :size="12" class="text-gray-500" />
472-
<span class="text-gray-700 truncate max-w-[100px]">{{ attachment.filename }}</span>
535+
<ImageIcon v-if="isImageAttachment(attachment)" :size="12" class="shrink-0" />
536+
<Paperclip v-else :size="12" class="shrink-0 text-gray-500" />
537+
<span class="truncate max-w-[120px] font-medium">{{ attachment.filename }}</span>
538+
<span class="shrink-0 opacity-70">{{ formatFileSize(attachment.size) }}</span>
473539
<button
474540
@click="handleRemoveAttachment(index)"
475541
class="text-gray-400 hover:text-red-500"
542+
:title="'移除附件'"
476543
>
477544
<X :size="12" />
478545
</button>
479546
</div>
480547
</div>
548+
<div
549+
v-if="showVisionWarning"
550+
class="mb-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800"
551+
>
552+
当前模型可能无法识别图片,请切换到 gpt-4o / gpt-4.1 系列 vision 模型,或发送文字描述。
553+
</div>
554+
<div
555+
v-if="uploadError"
556+
class="mb-2 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700"
557+
>
558+
{{ uploadError }}
559+
</div>
481560
<div class="flex items-center space-x-3 bg-white rounded-xl border border-gray-200 px-2 py-2 shadow-sm focus-within:ring-2 focus-within:ring-pink-500/20 focus-within:border-pink-500 transition-all">
482561
<button
483562
@click="handleClear"

agent-diva-gui/src/components/NormalMode.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ interface Message {
3434
isStreaming?: boolean;
3535
timestamp?: number;
3636
emotion?: string;
37-
attachments?: string[];
37+
attachments?: FileAttachmentDto[];
3838
}
3939
4040
interface ChatDisplayPrefs {
@@ -592,6 +592,7 @@ defineExpose({
592592
:is-typing="isTyping"
593593
:theme-mode="themeMode"
594594
:history-prefs="chatDisplayPrefs"
595+
:current-model="config?.model"
595596
@send="(content, attachments) => emit('send', content, attachments)"
596597
@clear="emit('clear')"
597598
@stop="emit('stop')"

0 commit comments

Comments
 (0)