Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Navigation menus are placed in nav_menus for easy reuse and unified management

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 || '',
},
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is related to configuring the global model, prompt_config should be done in a new separate API

};
saveAiConfig(params)
.then(() => {
Expand Down