Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ SUMMARY_MODEL="gpt-4o-mini"
EMBEDDING_MODEL="text-embedding-3-large"
CHAT_MODEL="gpt-5-mini"
FAST_MODEL="gpt-4o-mini"
TAB_CHAT_MODEL="gemini-2.0-flash-lite-preview"
EXTERNAL_VECTOR_BASE_URL=""
VECTOR_BACKEND="internal"
MAX_AI_RESPONSES_PER_TICKET="3"
Expand Down
77 changes: 52 additions & 25 deletions frontend/src/components/chat/message-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { type TicketType } from "tentix-server/rpc";
import {
Avatar,
AvatarFallback,
AvatarImage,
Badge,
Button,
Popover,
Expand All @@ -28,6 +27,8 @@ import {
} from "tentix-ui";
import useLocalUser from "../../hooks/use-local-user.tsx";
import { useChatStore, useSessionMembersStore } from "../../store/index.ts";
import { useUserCacheStore } from "@store/user-cache";
import { CachedAvatar } from "@comp/common/cached-avatar";
import ContentRenderer from "./content-renderer.tsx";
import { useTranslation } from "i18n";
import { memo, useState, useEffect } from "react";
Expand All @@ -39,13 +40,37 @@ interface MessageItemProps {
message: TicketType["messages"][number];
}


const useMessageSender = (message: TicketType["messages"][number]) => {
const { sessionMembers } = useSessionMembersStore();
const setUser = useUserCacheStore((state: any) => state.setUser);

const messageSender = sessionMembers?.find(
(member) => member.id === message.senderId,
);

// 缓存消息发送者信息
useEffect(() => {
if (messageSender) {
setUser({
id: messageSender.id,
name: messageSender.name,
nickname: messageSender.nickname || messageSender.name,
avatar: messageSender.avatar,
role: messageSender.role || 'unknown'
});
}
}, [messageSender, setUser]);

return messageSender;
};

// other's message component
const OtherMessage = ({
message,
}: {
message: TicketType["messages"][number];
}) => {
const { sessionMembers } = useSessionMembersStore();
const { role } = useLocalUser();
const { currentTicketId, updateMessage } = useChatStore();
const notCustomer = role !== "customer";
Expand All @@ -57,9 +82,8 @@ const OtherMessage = ({
const [feedbackComment, setFeedbackComment] = useState("");
const [hasComplaint, setHasComplaint] = useState(false);

const messageSender = sessionMembers?.find(
(member) => member.id === message.senderId,
);
// 使用提取的Hook
const messageSender = useMessageSender(message);

// Get current feedback status
const currentFeedback = message.feedbacks?.[0];
Expand Down Expand Up @@ -158,15 +182,17 @@ const OtherMessage = ({
return (
<div className="flex flex-col animate-fadeIn justify-start">
<div className="flex max-w-[85%] gap-3 min-w-0">
<Avatar className="h-8 w-8 shrink-0">
<AvatarImage
src={messageSender?.avatar}
alt={messageSender?.nickname ?? t("unknown")}
{messageSender ? (
<CachedAvatar
user={messageSender}
size="md"
showDebugInfo={import.meta.env.DEV}
/>
<AvatarFallback>
{messageSender?.nickname?.charAt(0) ?? "U"}
</AvatarFallback>
</Avatar>
) : (
<Avatar className="h-8 w-8 shrink-0">
<AvatarFallback>U</AvatarFallback>
</Avatar>
)}
<div
className={cn(
"flex flex-col gap-2 min-w-0 flex-1",
Expand Down Expand Up @@ -434,27 +460,28 @@ const MyMessage = ({
}: {
message: TicketType["messages"][number];
}) => {
const { sessionMembers } = useSessionMembersStore();
const { isMessageSending, withdrawMessageFunc: withdrawMessage, kbSelectionMode } =
useChatStore();
const { t } = useTranslation();

const messageSender = sessionMembers?.find(
(member) => member.id === message.senderId,
);
// 使用提取的Hook
const messageSender = useMessageSender(message);

return (
<div className="flex animate-fadeIn justify-end">
<div className="flex max-w-[85%] flex-row-reverse min-w-0">
<Avatar className="h-8 w-8 shrink-0 ml-3">
<AvatarImage
src={messageSender?.avatar}
alt={messageSender?.nickname ?? t("unknown")}
{messageSender ? (
<CachedAvatar
user={messageSender}
size="md"
className="ml-3"
showDebugInfo={import.meta.env.DEV}
/>
<AvatarFallback>
{messageSender?.nickname?.charAt(0) ?? "U"}
</AvatarFallback>
</Avatar>
) : (
<Avatar className="h-8 w-8 shrink-0 ml-3">
<AvatarFallback>U</AvatarFallback>
</Avatar>
)}
<div
className={cn(
"flex flex-col gap-2 rounded-xl py-4 px-5 ml-1 min-w-0 flex-1",
Expand Down
125 changes: 84 additions & 41 deletions frontend/src/components/chat/staff/message-input.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type JSONContentZod } from "tentix-server/types";
import { Loader2Icon, UploadIcon, LibraryBigIcon, XIcon } from "lucide-react";
import { Loader2Icon, UploadIcon, LibraryBigIcon, XIcon, FileTextIcon } from "lucide-react";
import React, { useRef, useState, useCallback, useMemo } from "react";
import {
SendIcon,
Expand All @@ -14,6 +14,8 @@ import useLocalUser from "@hook/use-local-user.tsx";
import { collectFavoritedKnowledge } from "@lib/query";
import { useTranslation } from "i18n";
import type { TFunction } from "i18next";
import { cn } from "../../../lib/utils";
import { ContextOrganizerDialog } from "../../../modal/use-context-organizer-modal";

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

const editorRef = useRef<EditorRef>(null);
const { toast } = useToast();
const { kbSelectionMode, clearKbSelection, selectedMessageIds } =
useChatStore();
const { id: userId } = useLocalUser();


const authToken = window.localStorage.getItem("token");
const ticketId = window.location.pathname.split('/').pop();

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

// 工单整理按钮点击
const handleContextOrganizerClick = useCallback(() => {
if (isLoading || uploadProgress) return;
setIsContextOrganizerOpen(true);
}, [isLoading, uploadProgress]);

const editorProps = useMemo(
() => ({
handleKeyDown: (_: unknown, event: KeyboardEvent) => {
Expand Down Expand Up @@ -336,6 +348,7 @@ export function StaffMessageInput({
};

const isUploading = uploadProgress !== null;
const isContextOrganizerDisabled = isLoading || isUploading;

if (kbSelectionMode) {
const count = selectedMessageIds.size;
Expand Down Expand Up @@ -410,45 +423,75 @@ export function StaffMessageInput({
}

return (
<div className="border-t relative">
{/* 上传进度指示器 */}
{renderUploadProgress()}

{/* 主要内容区域 - 动态调整顶部间距 */}
<form
onSubmit={handleSubmit}
style={{
marginTop: progressBarHeight,
transition: "margin-top 0.3s ease-in-out",
}}
>
<div className="flex">
<StaffChatEditor
ref={editorRef}
value={newMessage}
onChange={(value) => {
onTyping?.();
setNewMessage(value as JSONContentZod);
}}
throttleDelay={150}
editorContentClassName="overflow-auto h-full"
editable={!isUploading}
editorClassName="focus:outline-none p-4 h-full"
className="border-none"
editorProps={editorProps}
/>
</div>

<Button
type="submit"
size="icon"
className="absolute right-3 bottom-4 flex justify-center items-center rounded-[10px] bg-zinc-900 z-20 h-9 w-9"
disabled={!canSend}
<>
<div className="border-t relative">
{/* 上传进度指示器 */}
{renderUploadProgress()}

{/* 主要内容区域 - 动态调整顶部间距 */}
<form
onSubmit={handleSubmit}
style={{
marginTop: progressBarHeight,
transition: "margin-top 0.3s ease-in-out",
}}
>
{renderSendButtonContent()}
<span className="sr-only">{t("send_message_shortcut")}</span>
</Button>
</form>
</div>
<div className="flex">
<StaffChatEditor
ref={editorRef}
value={newMessage}
onChange={(value) => {
onTyping?.();
setNewMessage(value as JSONContentZod);
}}
throttleDelay={150}
editorContentClassName="overflow-auto h-full"
editable={!isUploading}
editorClassName="focus:outline-none p-4 h-full"
ticketId={ticketId || undefined}
authToken={authToken || undefined}
className="border-none"
editorProps={editorProps}
/>
</div>

{/* 工具栏区域 */}
<div className="absolute right-14 bottom-4 flex items-center gap-2 z-20">
{ticketId && authToken && (
<Button
variant="ghost"
size="sm"
onClick={handleContextOrganizerClick}
disabled={isContextOrganizerDisabled}
className={cn(
"h-9 w-9 p-0 hover:bg-gray-100"
)}
title={t('organize_ticket_context')}
>
<FileTextIcon className="h-4 w-4" />
</Button>
)}
</div>

<Button
type="submit"
size="icon"
className="absolute right-3 bottom-4 flex justify-center items-center rounded-[10px] bg-zinc-900 z-20 h-9 w-9"
disabled={!canSend}
>
{renderSendButtonContent()}
<span className="sr-only">{t("send_message_shortcut")}</span>
</Button>
</form>
</div>

{/* 工单整理对话框 */}
<ContextOrganizerDialog
open={isContextOrganizerOpen}
onOpenChange={setIsContextOrganizerOpen}
ticketId={ticketId || ""}
authToken={authToken || ""}
/>
</>
);
}
Loading