Skip to content

Commit dde5219

Browse files
committed
refactor: extract common aichatbot component which ready for reuse for other ai chat bot feature
1 parent e550e9c commit dde5219

File tree

4 files changed

+337
-298
lines changed

4 files changed

+337
-298
lines changed

src/client/components/ai-elements/context.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,3 +408,22 @@ const TokensWithCost = ({
408408
</span>
409409
);
410410
};
411+
412+
export const SimpleContextUsage = (
413+
props: Pick<ContextProps, 'maxTokens' | 'usage' | 'usedTokens'>
414+
) => {
415+
return (
416+
<Context {...props}>
417+
<ContextTrigger />
418+
<ContextContent>
419+
<ContextContentHeader />
420+
<ContextContentBody>
421+
<ContextInputUsage />
422+
<ContextOutputUsage />
423+
<ContextReasoningUsage />
424+
<ContextCacheUsage />
425+
</ContextContentBody>
426+
</ContextContent>
427+
</Context>
428+
);
429+
};
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import React from 'react';
2+
import { LuCircleAlert } from 'react-icons/lu';
3+
import { SimpleContextUsage } from '../ai-elements/context';
4+
import {
5+
Conversation,
6+
ConversationContent,
7+
ConversationScrollButton,
8+
} from '../ai-elements/conversation';
9+
import {
10+
PromptInput,
11+
PromptInputTextarea,
12+
PromptInputToolbar,
13+
PromptInputTools,
14+
PromptInputSubmit,
15+
} from '../ai-elements/prompt-input';
16+
import { Suggestions, Suggestion } from '../ai-elements/suggestion';
17+
import { AlertTitle, AlertDescription, Alert } from '../ui/alert';
18+
import { AIResponseMessages } from './AIResponseMessages';
19+
import { AbstractChat, ChatStatus, LanguageModelUsage, UIMessage } from 'ai';
20+
import { useTranslation } from '@i18next-toolkit/react';
21+
import { useGlobalConfig } from '@/hooks/useConfig';
22+
import { Button } from '../ui/button';
23+
import { cn } from '@/utils/style';
24+
25+
interface AIChatbotProps {
26+
className?: string;
27+
messages: UIMessage[];
28+
status: ChatStatus;
29+
error?: Error;
30+
input: string;
31+
setInput: (input: string) => void;
32+
placeholder?: string;
33+
usage?: LanguageModelUsage;
34+
isDisabled?: boolean;
35+
suggestions?: string[];
36+
alert?: React.ReactNode;
37+
selectedContext?: React.ReactNode;
38+
tools?: React.ReactNode;
39+
onSubmit?: (e: React.FormEvent<HTMLFormElement>) => void;
40+
onReset?: () => void;
41+
onRegenerate?: () => void;
42+
onSuggestionClick?: (suggestion: string) => void;
43+
onAddToolResult: (
44+
result: Parameters<AbstractChat<UIMessage>['addToolResult']>[0]
45+
) => void;
46+
}
47+
export const AIChatbot: React.FC<AIChatbotProps> = React.memo((props) => {
48+
const { t } = useTranslation();
49+
const { ai } = useGlobalConfig();
50+
51+
return (
52+
<div className={cn('flex flex-col', props.className)}>
53+
<Conversation>
54+
<ConversationContent className="space-y-2">
55+
<AIResponseMessages
56+
messages={props.messages}
57+
status={props.status}
58+
onRegenerate={props.onRegenerate}
59+
onAddToolResult={props.onAddToolResult}
60+
/>
61+
</ConversationContent>
62+
<ConversationScrollButton />
63+
</Conversation>
64+
65+
{props.error && (
66+
<div className="p-3">
67+
<Alert variant="destructive">
68+
<LuCircleAlert className="h-4 w-4" />
69+
<AlertTitle>{t('An error occurred.')}</AlertTitle>
70+
<AlertDescription>{String(props.error)}</AlertDescription>
71+
<div className="mt-2 flex items-center gap-2">
72+
<Button size="sm" variant="destructive" onClick={props.onReset}>
73+
{t('Retry')}
74+
</Button>
75+
</div>
76+
</Alert>
77+
</div>
78+
)}
79+
80+
{!props.isDisabled && (
81+
<Suggestions className="p-3 pb-0">
82+
{(props.suggestions ?? []).map((suggestion) => (
83+
<Suggestion
84+
key={suggestion}
85+
onClick={props.onSuggestionClick}
86+
suggestion={suggestion}
87+
/>
88+
))}
89+
</Suggestions>
90+
)}
91+
92+
<div className="p-3">
93+
{props.alert}
94+
95+
{props.selectedContext}
96+
97+
{props.usage &&
98+
props.status === 'ready' &&
99+
props.messages.length > 0 && (
100+
<div className="relative">
101+
<div className="absolute -top-10 right-0 px-4 py-1 text-right text-xs text-opacity-40">
102+
<SimpleContextUsage
103+
maxTokens={ai.contextWindow}
104+
usage={props.usage}
105+
usedTokens={props.usage.totalTokens ?? 0}
106+
/>
107+
</div>
108+
</div>
109+
)}
110+
111+
<PromptInput onSubmit={props.onSubmit}>
112+
<PromptInputTextarea
113+
value={props.input}
114+
onChange={(e) => props.setInput(e.target.value)}
115+
disabled={props.isDisabled}
116+
placeholder={props.placeholder}
117+
/>
118+
<PromptInputToolbar>
119+
<PromptInputTools>{props.tools}</PromptInputTools>
120+
<PromptInputSubmit
121+
disabled={
122+
!props.input &&
123+
['ready', 'submitted', 'error'].includes(props.status)
124+
}
125+
status={props.status}
126+
/>
127+
</PromptInputToolbar>
128+
</PromptInput>
129+
</div>
130+
</div>
131+
);
132+
});
133+
AIChatbot.displayName = 'AIChatbot';

src/client/components/ai/AIResponseMessages.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { useTranslation } from '@i18next-toolkit/react';
1515
interface AIResponseMessagesProps {
1616
messages: UIMessage<unknown, UIDataTypes, UITools>[];
1717
status: ChatStatus;
18-
onRegenerate: () => void;
18+
onRegenerate?: () => void;
1919
onAddToolResult: (
2020
result: Parameters<AbstractChat<UIMessage>['addToolResult']>[0]
2121
) => void;

0 commit comments

Comments
 (0)