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
8 changes: 6 additions & 2 deletions i18n/en_US.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2354,8 +2354,14 @@ ui:
model:
label: Model
msg: Model is required
prompt:
label: Prompt
text: Shows the prompt for the current language. Edit and save to apply.
add_success: AI settings updated successfully.
conversations:
tabs:
conversations: Conversations
settings: Settings
topic: Topic
helpful: Helpful
unhelpful: Unhelpful
Expand Down Expand Up @@ -2482,5 +2488,3 @@ ui:
copy: Copy to clipboard
copied: Copied
external_content_warning: External images/media are not displayed.


10 changes: 7 additions & 3 deletions i18n/zh_CN.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1796,7 +1796,7 @@ ui:
security: 安全
files: 文件
apikeys: API 密钥
intelligence: 智力
intelligence: 智能
ai_assistant: AI 助手
ai_settings: AI 设置
mcp: MCP
Expand Down Expand Up @@ -2318,8 +2318,14 @@ ui:
model:
label: 模型
msg: 模型是必需的
prompt:
label: 提示词
text: 显示当前语言环境的提示词,可在此修改并保存。
add_success: AI 设置更新成功。
conversations:
tabs:
conversations: 对话
settings: 设置
topic: 主题
helpful: 有帮助
unhelpful: 没有帮助
Expand Down Expand Up @@ -2446,5 +2452,3 @@ ui:
copy: 复制到剪贴板
copied: 已复制
external_content_warning: 外部图像/媒体未显示。


4 changes: 4 additions & 0 deletions ui/src/common/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,10 @@ export interface AiConfig {
api_key: string;
model: string;
}>;
prompt_config?: {
zh_cn: string;
en_us: string;
};
}

export interface AiProviderItem {
Expand Down
273 changes: 218 additions & 55 deletions ui/src/pages/Admin/AiAssistant/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,52 @@
* under the License.
*/

import { useState } from 'react';
import { Table, Button } from 'react-bootstrap';
import { FormEvent, useEffect, useRef, useState } from 'react';
import { Table, Button, Nav, Form } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'react-router-dom';

import { BaseUserCard, FormatTime, Pagination, Empty } from '@/components';
import { useQueryAdminConversationList } from '@/services';
import { useToast } from '@/hooks';
import {
getAiConfig,
saveAiConfig,
useQueryAdminConversationList,
} from '@/services';
import * as Type from '@/common/interface';

import DetailModal from './components/DetailModal';
import Action from './components/Action';

const getPromptByLang = (
promptConfig: Type.AiConfig['prompt_config'] | undefined,
lang: string,
) => {
if (!promptConfig) {
return '';
}
const isZh = lang?.toLowerCase().startsWith('zh');
return isZh ? promptConfig.zh_cn || '' : promptConfig.en_us || '';
};

const Index = () => {
const { t } = useTranslation('translation', {
const { t, i18n } = useTranslation('translation', {
keyPrefix: 'admin.conversations',
});
const toast = useToast();
const historyConfigRef = useRef<Type.AiConfig>();
const [urlSearchParams] = useSearchParams();
const curPage = Number(urlSearchParams.get('page') || '1');
const PAGE_SIZE = 20;
const [activeTab, setActiveTab] = useState<'conversations' | 'settings'>(
'conversations',
);
const [isSaving, setIsSaving] = useState(false);
const [promptForm, setPromptForm] = useState({
value: '',
isInvalid: false,
errorMsg: '',
});
const [detailModal, setDetailModal] = useState({
visible: false,
id: '',
Expand All @@ -47,6 +75,84 @@ const Index = () => {
page: curPage,
page_size: PAGE_SIZE,
});
const isZhLang = i18n.language?.toLowerCase().startsWith('zh');

const getAiConfigData = async () => {
const aiConfig = await getAiConfig();
historyConfigRef.current = aiConfig;
setPromptForm({
value: getPromptByLang(aiConfig.prompt_config, i18n.language),
isInvalid: false,
errorMsg: '',
});
};

const handleSavePrompt = (evt: FormEvent) => {
evt.preventDefault();
if (!historyConfigRef.current || isSaving) {
return;
}
setIsSaving(true);
setPromptForm((prev) => ({
...prev,
isInvalid: false,
errorMsg: '',
}));

const params: Type.AiConfig = {
enabled: historyConfigRef.current.enabled || false,
chosen_provider: historyConfigRef.current.chosen_provider || '',
ai_providers: historyConfigRef.current.ai_providers || [],
prompt_config: {
zh_cn: isZhLang
? promptForm.value
: historyConfigRef.current.prompt_config?.zh_cn || '',
en_us: isZhLang
? historyConfigRef.current.prompt_config?.en_us || ''
: promptForm.value,
},
};

saveAiConfig(params)
.then(() => {
historyConfigRef.current = params;
toast.onShow({
msg: t('update', { keyPrefix: 'toast' }),
variant: 'success',
});
})
.catch((err) => {
setPromptForm((prev) => ({
...prev,
isInvalid: true,
errorMsg: err?.message || '',
}));
})
.finally(() => {
setIsSaving(false);
});
};

useEffect(() => {
if (activeTab === 'settings') {
getAiConfigData();
}
}, [activeTab]);

useEffect(() => {
if (!historyConfigRef.current || activeTab !== 'settings') {
return;
}
setPromptForm((prev) => ({
...prev,
value: getPromptByLang(
historyConfigRef.current?.prompt_config,
i18n.language,
),
isInvalid: false,
errorMsg: '',
}));
}, [i18n.language, activeTab]);

const handleShowDetailModal = (data) => {
setDetailModal({
Expand All @@ -65,60 +171,117 @@ const Index = () => {
return (
<div className="d-flex flex-column flex-grow-1 position-relative">
<h3 className="mb-4">{t('ai_assistant', { keyPrefix: 'nav_menus' })}</h3>
<Table responsive="md">
<thead>
<tr>
<th className="min-w-15">{t('topic')}</th>
<th style={{ width: '10%' }}>{t('helpful')}</th>
<th style={{ width: '10%' }}>{t('unhelpful')}</th>
<th style={{ width: '20%' }}>{t('created')}</th>
<th style={{ width: '10%' }} className="text-end">
{t('action')}
</th>
</tr>
</thead>
<tbody className="align-middle">
{conversations?.list.map((item) => {
return (
<tr key={item.id}>
<td>
<Button
variant="link"
className="p-0 text-decoration-none text-truncate max-w-30"
onClick={() => handleShowDetailModal(item)}>
{item.topic}
</Button>
</td>
<td>{item.helpful_count}</td>
<td>{item.unhelpful_count}</td>
<td>
<div className="vstack">
<BaseUserCard data={item.user_info} avatarSize="20px" />
<FormatTime
className="small text-secondary"
time={item.created_at}
/>
</div>
</td>
<td className="text-end">
<Action id={item.id} refreshList={refreshList} />
</td>
<Nav variant="underline" className="mb-4 border-bottom">
<Nav.Item>
<Nav.Link
className="px-0 me-4"
active={activeTab === 'conversations'}
onClick={() => setActiveTab('conversations')}>
{t('tabs.conversations')}
</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link
className="px-0"
active={activeTab === 'settings'}
onClick={() => setActiveTab('settings')}>
{t('tabs.settings')}
</Nav.Link>
</Nav.Item>
</Nav>

{activeTab === 'conversations' && (
<>
<Table responsive="md">
<thead>
<tr>
<th className="min-w-15">{t('topic')}</th>
<th style={{ width: '10%' }}>{t('helpful')}</th>
<th style={{ width: '10%' }}>{t('unhelpful')}</th>
<th style={{ width: '20%' }}>{t('created')}</th>
<th style={{ width: '10%' }} className="text-end">
{t('action')}
</th>
</tr>
);
})}
</tbody>
</Table>
{!isLoading && Number(conversations?.count) <= 0 && (
<Empty>{t('empty')}</Empty>
</thead>
<tbody className="align-middle">
{conversations?.list.map((item) => {
return (
<tr key={item.id}>
<td>
<Button
variant="link"
className="p-0 text-decoration-none text-truncate max-w-30"
onClick={() => handleShowDetailModal(item)}>
{item.topic}
</Button>
</td>
<td>{item.helpful_count}</td>
<td>{item.unhelpful_count}</td>
<td>
<div className="vstack">
<BaseUserCard data={item.user_info} avatarSize="20px" />
<FormatTime
className="small text-secondary"
time={item.created_at}
/>
</div>
</td>
<td className="text-end">
<Action id={item.id} refreshList={refreshList} />
</td>
</tr>
);
})}
</tbody>
</Table>
{!isLoading && Number(conversations?.count) <= 0 && (
<Empty>{t('empty')}</Empty>
)}
<div className="mt-4 mb-2 d-flex justify-content-center">
<Pagination
currentPage={curPage}
totalSize={conversations?.count || 0}
pageSize={PAGE_SIZE}
/>
</div>
</>
)}

<div className="mt-4 mb-2 d-flex justify-content-center">
<Pagination
currentPage={curPage}
totalSize={conversations?.count || 0}
pageSize={PAGE_SIZE}
/>
</div>
{activeTab === 'settings' && (
<div className="max-w-748">
<Form noValidate onSubmit={handleSavePrompt}>
<div className="mb-3">
<label className="form-label" htmlFor="admin-prompt-textarea">
{t('prompt.label', { keyPrefix: 'admin.ai_settings' })}
</label>
<Form.Control
id="admin-prompt-textarea"
as="textarea"
rows={10}
isInvalid={promptForm.isInvalid}
value={promptForm.value}
onChange={(e) =>
setPromptForm({
value: e.target.value,
isInvalid: false,
errorMsg: '',
})
}
/>
<div className="form-text mt-1">
{t('prompt.text', { keyPrefix: 'admin.ai_settings' })}
</div>
<Form.Control.Feedback type="invalid">
{promptForm.errorMsg}
</Form.Control.Feedback>
</div>
<Button type="submit" className="btn-primary" disabled={isSaving}>
{t('save', { keyPrefix: 'btns' })}
</Button>
</Form>
</div>
)}
<DetailModal
visible={detailModal.visible}
id={detailModal.id}
Expand Down
4 changes: 4 additions & 0 deletions ui/src/pages/Admin/AiSettings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ const Index = () => {
enabled: formData.enabled.value,
chosen_provider: formData.provider.value,
ai_providers: newProviders,
prompt_config: {
zh_cn: historyConfigRef.current?.prompt_config?.zh_cn || '',
en_us: historyConfigRef.current?.prompt_config?.en_us || '',
},
};
saveAiConfig(params)
.then(() => {
Expand Down