Skip to content

Commit 71f2c8b

Browse files
committed
feat: add tab navigation, text cleanup and avatar caching
1 parent 8918e5a commit 71f2c8b

File tree

23 files changed

+2238
-105
lines changed

23 files changed

+2238
-105
lines changed

.env.template

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ SUMMARY_MODEL="gpt-4o-mini"
1414
EMBEDDING_MODEL="text-embedding-3-large"
1515
CHAT_MODEL="gpt-5-mini"
1616
FAST_MODEL="gpt-4o-mini"
17+
TAB_CHAT_MODEL="gemini-2.0-flash-lite-preview"
1718
EXTERNAL_VECTOR_BASE_URL=""
1819
VECTOR_BACKEND="internal"
1920
MAX_AI_RESPONSES_PER_TICKET="3"

frontend/src/components/chat/message-item.tsx

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { type TicketType } from "tentix-server/rpc";
1313
import {
1414
Avatar,
1515
AvatarFallback,
16-
AvatarImage,
1716
Badge,
1817
Button,
1918
Popover,
@@ -28,6 +27,8 @@ import {
2827
} from "tentix-ui";
2928
import useLocalUser from "../../hooks/use-local-user.tsx";
3029
import { useChatStore, useSessionMembersStore } from "../../store/index.ts";
30+
import { useUserCacheStore } from "@store/user-cache";
31+
import { CachedAvatar } from "@comp/common/cached-avatar";
3132
import ContentRenderer from "./content-renderer.tsx";
3233
import { useTranslation } from "i18n";
3334
import { memo, useState, useEffect } from "react";
@@ -39,13 +40,37 @@ interface MessageItemProps {
3940
message: TicketType["messages"][number];
4041
}
4142

43+
44+
const useMessageSender = (message: TicketType["messages"][number]) => {
45+
const { sessionMembers } = useSessionMembersStore();
46+
const setUser = useUserCacheStore((state: any) => state.setUser);
47+
48+
const messageSender = sessionMembers?.find(
49+
(member) => member.id === message.senderId,
50+
);
51+
52+
// 缓存消息发送者信息
53+
useEffect(() => {
54+
if (messageSender) {
55+
setUser({
56+
id: messageSender.id,
57+
name: messageSender.name,
58+
nickname: messageSender.nickname || messageSender.name,
59+
avatar: messageSender.avatar,
60+
role: messageSender.role || 'unknown'
61+
});
62+
}
63+
}, [messageSender, setUser]);
64+
65+
return messageSender;
66+
};
67+
4268
// other's message component
4369
const OtherMessage = ({
4470
message,
4571
}: {
4672
message: TicketType["messages"][number];
4773
}) => {
48-
const { sessionMembers } = useSessionMembersStore();
4974
const { role } = useLocalUser();
5075
const { currentTicketId, updateMessage } = useChatStore();
5176
const notCustomer = role !== "customer";
@@ -57,9 +82,8 @@ const OtherMessage = ({
5782
const [feedbackComment, setFeedbackComment] = useState("");
5883
const [hasComplaint, setHasComplaint] = useState(false);
5984

60-
const messageSender = sessionMembers?.find(
61-
(member) => member.id === message.senderId,
62-
);
85+
// 使用提取的Hook
86+
const messageSender = useMessageSender(message);
6387

6488
// Get current feedback status
6589
const currentFeedback = message.feedbacks?.[0];
@@ -158,15 +182,17 @@ const OtherMessage = ({
158182
return (
159183
<div className="flex flex-col animate-fadeIn justify-start">
160184
<div className="flex max-w-[85%] gap-3 min-w-0">
161-
<Avatar className="h-8 w-8 shrink-0">
162-
<AvatarImage
163-
src={messageSender?.avatar}
164-
alt={messageSender?.nickname ?? t("unknown")}
185+
{messageSender ? (
186+
<CachedAvatar
187+
user={messageSender}
188+
size="md"
189+
showDebugInfo={import.meta.env.DEV}
165190
/>
166-
<AvatarFallback>
167-
{messageSender?.nickname?.charAt(0) ?? "U"}
168-
</AvatarFallback>
169-
</Avatar>
191+
) : (
192+
<Avatar className="h-8 w-8 shrink-0">
193+
<AvatarFallback>U</AvatarFallback>
194+
</Avatar>
195+
)}
170196
<div
171197
className={cn(
172198
"flex flex-col gap-2 min-w-0 flex-1",
@@ -434,27 +460,28 @@ const MyMessage = ({
434460
}: {
435461
message: TicketType["messages"][number];
436462
}) => {
437-
const { sessionMembers } = useSessionMembersStore();
438463
const { isMessageSending, withdrawMessageFunc: withdrawMessage, kbSelectionMode } =
439464
useChatStore();
440465
const { t } = useTranslation();
441466

442-
const messageSender = sessionMembers?.find(
443-
(member) => member.id === message.senderId,
444-
);
467+
// 使用提取的Hook
468+
const messageSender = useMessageSender(message);
445469

446470
return (
447471
<div className="flex animate-fadeIn justify-end">
448472
<div className="flex max-w-[85%] flex-row-reverse min-w-0">
449-
<Avatar className="h-8 w-8 shrink-0 ml-3">
450-
<AvatarImage
451-
src={messageSender?.avatar}
452-
alt={messageSender?.nickname ?? t("unknown")}
473+
{messageSender ? (
474+
<CachedAvatar
475+
user={messageSender}
476+
size="md"
477+
className="ml-3"
478+
showDebugInfo={import.meta.env.DEV}
453479
/>
454-
<AvatarFallback>
455-
{messageSender?.nickname?.charAt(0) ?? "U"}
456-
</AvatarFallback>
457-
</Avatar>
480+
) : (
481+
<Avatar className="h-8 w-8 shrink-0 ml-3">
482+
<AvatarFallback>U</AvatarFallback>
483+
</Avatar>
484+
)}
458485
<div
459486
className={cn(
460487
"flex flex-col gap-2 rounded-xl py-4 px-5 ml-1 min-w-0 flex-1",

frontend/src/components/chat/staff/message-input.tsx

Lines changed: 84 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type JSONContentZod } from "tentix-server/types";
2-
import { Loader2Icon, UploadIcon, LibraryBigIcon, XIcon } from "lucide-react";
2+
import { Loader2Icon, UploadIcon, LibraryBigIcon, XIcon, FileTextIcon } from "lucide-react";
33
import React, { useRef, useState, useCallback, useMemo } from "react";
44
import {
55
SendIcon,
@@ -14,6 +14,8 @@ import useLocalUser from "@hook/use-local-user.tsx";
1414
import { collectFavoritedKnowledge } from "@lib/query";
1515
import { useTranslation } from "i18n";
1616
import type { TFunction } from "i18next";
17+
import { cn } from "../../../lib/utils";
18+
import { ContextOrganizerDialog } from "../../../modal/use-context-organizer-modal";
1719

1820
// 错误处理工具函数
1921
const getErrorMessage = (error: unknown, t: TFunction): string => {
@@ -139,14 +141,18 @@ export function StaffMessageInput({
139141
const [uploadProgress, setUploadProgress] = useState<UploadProgress | null>(
140142
null,
141143
);
144+
const [isContextOrganizerOpen, setIsContextOrganizerOpen] = useState(false);
142145

143146
const editorRef = useRef<EditorRef>(null);
144147
const { toast } = useToast();
145148
const { kbSelectionMode, clearKbSelection, selectedMessageIds } =
146149
useChatStore();
147150
const { id: userId } = useLocalUser();
151+
152+
153+
const authToken = window.localStorage.getItem("token");
154+
const ticketId = window.location.pathname.split('/').pop();
148155

149-
// 分析消息内容中的文件情况
150156
const analyzeFileContent = useCallback(
151157
(content: JSONContentZod): FileStats => {
152158
let count = 0;
@@ -257,6 +263,12 @@ export function StaffMessageInput({
257263
],
258264
);
259265

266+
// 工单整理按钮点击
267+
const handleContextOrganizerClick = useCallback(() => {
268+
if (isLoading || uploadProgress) return;
269+
setIsContextOrganizerOpen(true);
270+
}, [isLoading, uploadProgress]);
271+
260272
const editorProps = useMemo(
261273
() => ({
262274
handleKeyDown: (_: unknown, event: KeyboardEvent) => {
@@ -336,6 +348,7 @@ export function StaffMessageInput({
336348
};
337349

338350
const isUploading = uploadProgress !== null;
351+
const isContextOrganizerDisabled = isLoading || isUploading;
339352

340353
if (kbSelectionMode) {
341354
const count = selectedMessageIds.size;
@@ -410,45 +423,75 @@ export function StaffMessageInput({
410423
}
411424

412425
return (
413-
<div className="border-t relative">
414-
{/* 上传进度指示器 */}
415-
{renderUploadProgress()}
416-
417-
{/* 主要内容区域 - 动态调整顶部间距 */}
418-
<form
419-
onSubmit={handleSubmit}
420-
style={{
421-
marginTop: progressBarHeight,
422-
transition: "margin-top 0.3s ease-in-out",
423-
}}
424-
>
425-
<div className="flex">
426-
<StaffChatEditor
427-
ref={editorRef}
428-
value={newMessage}
429-
onChange={(value) => {
430-
onTyping?.();
431-
setNewMessage(value as JSONContentZod);
432-
}}
433-
throttleDelay={150}
434-
editorContentClassName="overflow-auto h-full"
435-
editable={!isUploading}
436-
editorClassName="focus:outline-none p-4 h-full"
437-
className="border-none"
438-
editorProps={editorProps}
439-
/>
440-
</div>
441-
442-
<Button
443-
type="submit"
444-
size="icon"
445-
className="absolute right-3 bottom-4 flex justify-center items-center rounded-[10px] bg-zinc-900 z-20 h-9 w-9"
446-
disabled={!canSend}
426+
<>
427+
<div className="border-t relative">
428+
{/* 上传进度指示器 */}
429+
{renderUploadProgress()}
430+
431+
{/* 主要内容区域 - 动态调整顶部间距 */}
432+
<form
433+
onSubmit={handleSubmit}
434+
style={{
435+
marginTop: progressBarHeight,
436+
transition: "margin-top 0.3s ease-in-out",
437+
}}
447438
>
448-
{renderSendButtonContent()}
449-
<span className="sr-only">{t("send_message_shortcut")}</span>
450-
</Button>
451-
</form>
452-
</div>
439+
<div className="flex">
440+
<StaffChatEditor
441+
ref={editorRef}
442+
value={newMessage}
443+
onChange={(value) => {
444+
onTyping?.();
445+
setNewMessage(value as JSONContentZod);
446+
}}
447+
throttleDelay={150}
448+
editorContentClassName="overflow-auto h-full"
449+
editable={!isUploading}
450+
editorClassName="focus:outline-none p-4 h-full"
451+
ticketId={ticketId || undefined}
452+
authToken={authToken || undefined}
453+
className="border-none"
454+
editorProps={editorProps}
455+
/>
456+
</div>
457+
458+
{/* 工具栏区域 */}
459+
<div className="absolute right-14 bottom-4 flex items-center gap-2 z-20">
460+
{ticketId && authToken && (
461+
<Button
462+
variant="ghost"
463+
size="sm"
464+
onClick={handleContextOrganizerClick}
465+
disabled={isContextOrganizerDisabled}
466+
className={cn(
467+
"h-9 w-9 p-0 hover:bg-gray-100"
468+
)}
469+
title={t('organize_ticket_context')}
470+
>
471+
<FileTextIcon className="h-4 w-4" />
472+
</Button>
473+
)}
474+
</div>
475+
476+
<Button
477+
type="submit"
478+
size="icon"
479+
className="absolute right-3 bottom-4 flex justify-center items-center rounded-[10px] bg-zinc-900 z-20 h-9 w-9"
480+
disabled={!canSend}
481+
>
482+
{renderSendButtonContent()}
483+
<span className="sr-only">{t("send_message_shortcut")}</span>
484+
</Button>
485+
</form>
486+
</div>
487+
488+
{/* 工单整理对话框 */}
489+
<ContextOrganizerDialog
490+
open={isContextOrganizerOpen}
491+
onOpenChange={setIsContextOrganizerOpen}
492+
ticketId={ticketId || ""}
493+
authToken={authToken || ""}
494+
/>
495+
</>
453496
);
454497
}

0 commit comments

Comments
 (0)