mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-15 12:10:16 +00:00
💄 style: refactor the plugin render style (#6390)
* wip * refactor the args design * update * refactor tools * try improve suspense * update i18n * fix lint * fix api key issue * improve code * improve suspense * improve suspense * fix topic duplicate issue
This commit is contained in:
@@ -118,6 +118,10 @@
|
||||
"reinstallError": "فشل تحديث الإضافة {{name}}",
|
||||
"urlError": "الرابط لا يعيد محتوى بتنسيق JSON، يرجى التأكد من صحة الرابط"
|
||||
},
|
||||
"inspector": {
|
||||
"args": "عرض قائمة المعلمات",
|
||||
"pluginRender": "عرض واجهة المكون الإضافي"
|
||||
},
|
||||
"list": {
|
||||
"item": {
|
||||
"deprecated.title": "مهجور",
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
"reinstallError": "Неуспешно опресняване на плъгина {{name}}",
|
||||
"urlError": "Връзката не върна съдържание във формат JSON. Моля, уверете се, че е валидна връзка."
|
||||
},
|
||||
"inspector": {
|
||||
"args": "Преглед на списъка с параметри",
|
||||
"pluginRender": "Преглед на интерфейса на плъгина"
|
||||
},
|
||||
"list": {
|
||||
"item": {
|
||||
"deprecated.title": "Изтрит",
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
"reinstallError": "Fehler beim Aktualisieren des Plugins {{name}}.",
|
||||
"urlError": "Der Link hat keine JSON-Format-Inhalte zurückgegeben. Stellen Sie sicher, dass der Link gültig ist."
|
||||
},
|
||||
"inspector": {
|
||||
"args": "Parameterliste anzeigen",
|
||||
"pluginRender": "Plugin-Oberfläche anzeigen"
|
||||
},
|
||||
"list": {
|
||||
"item": {
|
||||
"deprecated.title": "Veraltet",
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
"reinstallError": "Failed to refresh plugin {{name}}",
|
||||
"urlError": "The link did not return content in JSON format. Please ensure it is a valid link."
|
||||
},
|
||||
"inspector": {
|
||||
"args": "View parameter list",
|
||||
"pluginRender": "View plugin interface"
|
||||
},
|
||||
"list": {
|
||||
"item": {
|
||||
"deprecated.title": "Deleted",
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
"reinstallError": "Error al volver a instalar el complemento {{name}}.",
|
||||
"urlError": "El enlace no devuelve contenido en formato JSON. Asegúrese de que sea un enlace válido."
|
||||
},
|
||||
"inspector": {
|
||||
"args": "Ver lista de parámetros",
|
||||
"pluginRender": "Ver interfaz del plugin"
|
||||
},
|
||||
"list": {
|
||||
"item": {
|
||||
"deprecated.title": "Obsoleto",
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
"reinstallError": "بروزرسانی افزونه {{name}} ناموفق بود.",
|
||||
"urlError": "این لینک محتوای JSON بازنگرداند، لطفاً از معتبر بودن لینک اطمینان حاصل کنید."
|
||||
},
|
||||
"inspector": {
|
||||
"args": "مشاهده لیست پارامترها",
|
||||
"pluginRender": "مشاهده رابط کاربری پلاگین"
|
||||
},
|
||||
"list": {
|
||||
"item": {
|
||||
"deprecated.title": "حذف شده",
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
"reinstallError": "Échec de la mise à jour du plugin {{name}}",
|
||||
"urlError": "Ce lien ne renvoie pas de contenu au format JSON. Veuillez vous assurer qu'il s'agit d'un lien valide."
|
||||
},
|
||||
"inspector": {
|
||||
"args": "Voir la liste des paramètres",
|
||||
"pluginRender": "Voir l'interface du plugin"
|
||||
},
|
||||
"list": {
|
||||
"item": {
|
||||
"deprecated.title": "Obsolète",
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
"reinstallError": "Ricaricamento del plugin {{name}} fallito",
|
||||
"urlError": "Il collegamento non restituisce contenuti nel formato JSON. Assicurati che il collegamento sia valido"
|
||||
},
|
||||
"inspector": {
|
||||
"args": "Visualizza l'elenco dei parametri",
|
||||
"pluginRender": "Visualizza l'interfaccia del plugin"
|
||||
},
|
||||
"list": {
|
||||
"item": {
|
||||
"deprecated.title": "Deprecato",
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
"reinstallError": "プラグイン{{name}}の再インストールに失敗しました",
|
||||
"urlError": "このリンクはJSON形式のコンテンツを返していません。有効なリンクであることを確認してください"
|
||||
},
|
||||
"inspector": {
|
||||
"args": "パラメーターリストを表示",
|
||||
"pluginRender": "プラグインインターフェースを表示"
|
||||
},
|
||||
"list": {
|
||||
"item": {
|
||||
"deprecated.title": "削除済み",
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
"reinstallError": "플러그인 {{name}} 다시 설치 중 오류가 발생했습니다.",
|
||||
"urlError": "이 링크는 JSON 형식의 내용을 반환하지 않습니다. 유효한 링크인지 확인하세요."
|
||||
},
|
||||
"inspector": {
|
||||
"args": "매개변수 목록 보기",
|
||||
"pluginRender": "플러그인 인터페이스 보기"
|
||||
},
|
||||
"list": {
|
||||
"item": {
|
||||
"deprecated.title": "삭제됨",
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
"reinstallError": "Vernieuwen van de plugin {{name}} is mislukt.",
|
||||
"urlError": "De link retourneert geen JSON-indeling. Zorg ervoor dat het een geldige link is."
|
||||
},
|
||||
"inspector": {
|
||||
"args": "Bekijk parameterlijst",
|
||||
"pluginRender": "Bekijk plugininterface"
|
||||
},
|
||||
"list": {
|
||||
"item": {
|
||||
"deprecated.title": "Verouderd",
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
"reinstallError": "Nie udało się odświeżyć wtyczki {{name}}",
|
||||
"urlError": "Link nie zwrócił treści w formacie JSON. Upewnij się, że jest to poprawny link."
|
||||
},
|
||||
"inspector": {
|
||||
"args": "Zobacz listę parametrów",
|
||||
"pluginRender": "Zobacz interfejs wtyczki"
|
||||
},
|
||||
"list": {
|
||||
"item": {
|
||||
"deprecated.title": "Usunięte",
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
"reinstallError": "Falha ao atualizar o plugin {{name}}",
|
||||
"urlError": "O link não retornou conteúdo no formato JSON. Certifique-se de que o link é válido."
|
||||
},
|
||||
"inspector": {
|
||||
"args": "Ver parâmetros",
|
||||
"pluginRender": "Ver interface do plugin"
|
||||
},
|
||||
"list": {
|
||||
"item": {
|
||||
"deprecated.title": "Obsoleto",
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
"reinstallError": "Ошибка при обновлении плагина {{name}}",
|
||||
"urlError": "Ссылка не возвращает данные в формате JSON. Проверьте правильность ссылки"
|
||||
},
|
||||
"inspector": {
|
||||
"args": "Просмотреть список параметров",
|
||||
"pluginRender": "Просмотреть интерфейс плагина"
|
||||
},
|
||||
"list": {
|
||||
"item": {
|
||||
"deprecated.title": "Устарел",
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
"reinstallError": "{{name}} eklentisi yenilenemedi",
|
||||
"urlError": "Bağlantı JSON formatında içerik döndürmedi. Lütfen geçerli bir bağlantı olduğundan emin olun"
|
||||
},
|
||||
"inspector": {
|
||||
"args": "Parametre listesini görüntüle",
|
||||
"pluginRender": "Eklenti arayüzünü görüntüle"
|
||||
},
|
||||
"list": {
|
||||
"item": {
|
||||
"deprecated.title": "Eski",
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
"reinstallError": "Làm mới plugin {{name}} thất bại",
|
||||
"urlError": "Liên kết này không trả về nội dung dạng JSON, vui lòng đảm bảo rằng đó là một liên kết hợp lệ"
|
||||
},
|
||||
"inspector": {
|
||||
"args": "Xem danh sách tham số",
|
||||
"pluginRender": "Xem giao diện plugin"
|
||||
},
|
||||
"list": {
|
||||
"item": {
|
||||
"deprecated.title": "Đã loại bỏ",
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
"reinstallError": "插件 {{name}} 刷新失败",
|
||||
"urlError": "该链接没有返回 JSON 格式的内容, 请确保是有效的链接"
|
||||
},
|
||||
"inspector": {
|
||||
"args": "查看参数列表",
|
||||
"pluginRender": "查看插件界面"
|
||||
},
|
||||
"list": {
|
||||
"item": {
|
||||
"deprecated.title": "已删除",
|
||||
@@ -163,4 +167,4 @@
|
||||
"title": "插件商店"
|
||||
},
|
||||
"unknownPlugin": "未知插件"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
"reinstallError": "插件 {{name}} 刷新失敗",
|
||||
"urlError": "該連結沒有返回 JSON 格式的內容, 請確保是有效的連結"
|
||||
},
|
||||
"inspector": {
|
||||
"args": "查看參數列表",
|
||||
"pluginRender": "查看插件介面"
|
||||
},
|
||||
"list": {
|
||||
"item": {
|
||||
"deprecated.title": "已刪除",
|
||||
|
||||
+1
-1
@@ -14,10 +14,10 @@ const leftActions = [
|
||||
'model',
|
||||
'fileUpload',
|
||||
'knowledgeBase',
|
||||
'params',
|
||||
'history',
|
||||
'stt',
|
||||
'tools',
|
||||
'params',
|
||||
'mainToken',
|
||||
] as ActionKeys[];
|
||||
|
||||
|
||||
@@ -115,21 +115,34 @@ export class AiProviderModel {
|
||||
const encrypt = encryptor ?? defaultSerialize;
|
||||
const keyVaults = await encrypt(JSON.stringify(value.keyVaults));
|
||||
|
||||
const commonFields = {
|
||||
checkModel: value.checkModel,
|
||||
fetchOnClient: value.fetchOnClient,
|
||||
keyVaults,
|
||||
};
|
||||
|
||||
return this.db
|
||||
.update(aiProviders)
|
||||
.set({ ...value, keyVaults, updatedAt: new Date() })
|
||||
.where(and(eq(aiProviders.id, id), eq(aiProviders.userId, this.userId)));
|
||||
.insert(aiProviders)
|
||||
.values({
|
||||
...commonFields,
|
||||
id,
|
||||
source: this.getProviderSource(id),
|
||||
updatedAt: new Date(),
|
||||
userId: this.userId,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
set: { ...commonFields, updatedAt: new Date() },
|
||||
target: [aiProviders.id, aiProviders.userId],
|
||||
});
|
||||
};
|
||||
|
||||
toggleProviderEnabled = async (id: string, enabled: boolean) => {
|
||||
const isBuiltin = Object.values(ModelProvider).includes(id as any);
|
||||
|
||||
return this.db
|
||||
.insert(aiProviders)
|
||||
.values({
|
||||
enabled,
|
||||
id,
|
||||
source: isBuiltin ? 'builtin' : 'custom',
|
||||
source: this.getProviderSource(id),
|
||||
updatedAt: new Date(),
|
||||
userId: this.userId,
|
||||
})
|
||||
@@ -142,15 +155,13 @@ export class AiProviderModel {
|
||||
updateOrder = async (sortMap: { id: string; sort: number }[]) => {
|
||||
await this.db.transaction(async (tx) => {
|
||||
const updates = sortMap.map(({ id, sort }) => {
|
||||
const isBuiltin = Object.values(ModelProvider).includes(id as any);
|
||||
|
||||
return tx
|
||||
.insert(aiProviders)
|
||||
.values({
|
||||
enabled: true,
|
||||
id,
|
||||
sort,
|
||||
source: isBuiltin ? 'builtin' : 'custom',
|
||||
source: this.getProviderSource(id),
|
||||
updatedAt: new Date(),
|
||||
userId: this.userId,
|
||||
})
|
||||
@@ -238,4 +249,6 @@ export class AiProviderModel {
|
||||
};
|
||||
|
||||
private isBuiltInProvider = (id: string) => Object.values(ModelProvider).includes(id as any);
|
||||
|
||||
private getProviderSource = (id: string) => (this.isBuiltInProvider(id) ? 'builtin' : 'custom');
|
||||
}
|
||||
|
||||
@@ -224,6 +224,7 @@ export class TopicModel {
|
||||
.insert(topics)
|
||||
.values({
|
||||
...originalTopic,
|
||||
clientId: null,
|
||||
id: this.genId(),
|
||||
title: newTitle || originalTopic?.title,
|
||||
})
|
||||
@@ -242,6 +243,7 @@ export class TopicModel {
|
||||
.insert(messages)
|
||||
.values({
|
||||
...message,
|
||||
clientId: null,
|
||||
id: idGenerator('messages'),
|
||||
topicId: duplicatedTopic.id,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Highlighter } from '@lobehub/ui';
|
||||
import { Tabs } from 'antd';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface DebugProps {
|
||||
payload: object;
|
||||
requestArgs?: string;
|
||||
}
|
||||
|
||||
const Debug = memo<DebugProps>(({ payload, requestArgs }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
let params;
|
||||
try {
|
||||
params = JSON.stringify(JSON.parse(requestArgs || ''), null, 2);
|
||||
} catch {
|
||||
params = '';
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
children: <Highlighter language={'json'}>{params}</Highlighter>,
|
||||
key: 'arguments',
|
||||
label: t('debug.arguments'),
|
||||
},
|
||||
{
|
||||
children: <Highlighter language={'json'}>{JSON.stringify(payload, null, 2)}</Highlighter>,
|
||||
key: 'function_call',
|
||||
label: t('debug.function_call'),
|
||||
},
|
||||
// {
|
||||
// children: <PluginResult content={content} />,
|
||||
// key: 'response',
|
||||
// label: t('debug.response'),
|
||||
// },
|
||||
]}
|
||||
style={{ display: 'grid', maxWidth: 800, minWidth: 400 }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
export default Debug;
|
||||
@@ -0,0 +1,58 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
|
||||
export const useStyles = createStyles(({ css, token }, borderWidth: number = 2.5) => ({
|
||||
background: css`
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
aspect-ratio: 1;
|
||||
width: 100%;
|
||||
border-radius: 50%;
|
||||
|
||||
background: ${token.colorFill};
|
||||
|
||||
mask: radial-gradient(farthest-side, #0000 calc(100% - ${borderWidth}px), #000 0);
|
||||
`,
|
||||
container: css`
|
||||
position: relative;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
`,
|
||||
|
||||
loader: css`
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
aspect-ratio: 1;
|
||||
width: 100%;
|
||||
border-radius: 50%;
|
||||
|
||||
background:
|
||||
radial-gradient(farthest-side, ${token.colorTextSecondary} 94%, #0000) top/ ${borderWidth}px
|
||||
${borderWidth}px no-repeat,
|
||||
conic-gradient(#0000 50%, ${token.colorTextSecondary});
|
||||
|
||||
mask: radial-gradient(farthest-side, #0000 calc(100% - ${borderWidth}px), #000 0);
|
||||
|
||||
animation: small-loader-anim 1s infinite linear;
|
||||
|
||||
@keyframes small-loader-anim {
|
||||
100% {
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
`,
|
||||
}));
|
||||
|
||||
const Loader = () => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.loader} />
|
||||
<div className={styles.background} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loader;
|
||||
@@ -0,0 +1,151 @@
|
||||
import { ActionIcon, Icon } from '@lobehub/ui';
|
||||
import { createStyles } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
LayoutPanelTop,
|
||||
LogsIcon,
|
||||
LucideBug,
|
||||
LucideBugOff,
|
||||
} from 'lucide-react';
|
||||
import { CSSProperties, memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import PluginAvatar from '@/features/PluginAvatar';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { chatSelectors } from '@/store/chat/selectors';
|
||||
import { pluginHelpers, useToolStore } from '@/store/tool';
|
||||
import { toolSelectors } from '@/store/tool/selectors';
|
||||
import { shinyTextStylish } from '@/styles/loading';
|
||||
|
||||
import Debug from './Debug';
|
||||
import Loader from './Loader';
|
||||
import Settings from './Settings';
|
||||
|
||||
export const useStyles = createStyles(({ css, token }) => ({
|
||||
apiName: css`
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
|
||||
font-family: ${token.fontFamilyCode};
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
`,
|
||||
container: css`
|
||||
cursor: pointer;
|
||||
|
||||
width: fit-content;
|
||||
padding-block: 2px;
|
||||
border-radius: 6px;
|
||||
|
||||
color: ${token.colorTextTertiary};
|
||||
|
||||
&:hover {
|
||||
background: ${token.colorFillTertiary};
|
||||
}
|
||||
`,
|
||||
plugin: css`
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
`,
|
||||
shinyText: shinyTextStylish(token),
|
||||
}));
|
||||
|
||||
interface InspectorProps {
|
||||
apiName: string;
|
||||
arguments?: string;
|
||||
id: string;
|
||||
identifier: string;
|
||||
index: number;
|
||||
messageId: string;
|
||||
payload: object;
|
||||
setShowPluginRender: (show: boolean) => void;
|
||||
setShowRender: (show: boolean) => void;
|
||||
showPluginRender: boolean;
|
||||
showPortal?: boolean;
|
||||
showRender: boolean;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
const Inspectors = memo<InspectorProps>(
|
||||
({
|
||||
messageId,
|
||||
index,
|
||||
identifier,
|
||||
apiName,
|
||||
arguments: requestArgs,
|
||||
showRender,
|
||||
payload,
|
||||
setShowRender,
|
||||
showPluginRender,
|
||||
setShowPluginRender,
|
||||
}) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const { styles } = useStyles();
|
||||
|
||||
const [showDebug, setShowDebug] = useState(false);
|
||||
|
||||
const loading = useChatStore(chatSelectors.isToolCallStreaming(messageId, index));
|
||||
|
||||
const pluginMeta = useToolStore(toolSelectors.getMetaById(identifier), isEqual);
|
||||
const pluginTitle = pluginHelpers.getPluginTitle(pluginMeta) ?? t('unknownPlugin');
|
||||
|
||||
return (
|
||||
<Flexbox gap={4}>
|
||||
<Flexbox align={'center'} distribution={'space-between'} gap={8} horizontal>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={styles.container}
|
||||
gap={8}
|
||||
horizontal
|
||||
onClick={() => {
|
||||
setShowRender(!showRender);
|
||||
}}
|
||||
paddingInline={4}
|
||||
>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={loading ? styles.shinyText : ''}
|
||||
gap={4}
|
||||
horizontal
|
||||
>
|
||||
{loading ? <Loader /> : <PluginAvatar identifier={identifier} size={20} />}
|
||||
<div>{pluginTitle}</div>/<span className={styles.apiName}>{apiName}</span>
|
||||
</Flexbox>
|
||||
<Icon icon={showRender ? ChevronDown : ChevronRight} />
|
||||
</Flexbox>
|
||||
<Flexbox horizontal>
|
||||
{showRender && (
|
||||
<ActionIcon
|
||||
icon={showPluginRender ? LogsIcon : LayoutPanelTop}
|
||||
onClick={() => {
|
||||
setShowPluginRender(!showPluginRender);
|
||||
}}
|
||||
size={'small'}
|
||||
title={showPluginRender ? t('inspector.args') : t('inspector.pluginRender')}
|
||||
/>
|
||||
)}
|
||||
<ActionIcon
|
||||
icon={showDebug ? LucideBugOff : LucideBug}
|
||||
onClick={() => {
|
||||
setShowDebug(!showDebug);
|
||||
}}
|
||||
size={'small'}
|
||||
title={t(showDebug ? 'debug.off' : 'debug.on')}
|
||||
/>
|
||||
<Settings id={identifier} />
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
{showDebug && <Debug payload={payload} requestArgs={requestArgs} />}
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default Inspectors;
|
||||
@@ -0,0 +1,165 @@
|
||||
import { Highlighter, copyToClipboard } from '@lobehub/ui';
|
||||
import { App } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { parse } from 'partial-json';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useIsMobile } from '@/hooks/useIsMobile';
|
||||
import { useYamlArguments } from '@/hooks/useYamlArguments';
|
||||
import { shinyTextStylish } from '@/styles/loading';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
arrayRow: css`
|
||||
&:not(:first-child) {
|
||||
border-block-start: 1px dotted ${token.colorBorderSecondary};
|
||||
}
|
||||
`,
|
||||
colon: css`
|
||||
color: ${token.colorTextTertiary};
|
||||
`,
|
||||
container: css`
|
||||
padding-block: 4px;
|
||||
padding-inline: 12px;
|
||||
border-radius: ${token.borderRadiusLG}px;
|
||||
|
||||
font-family: ${token.fontFamilyCode};
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
|
||||
background: ${token.colorFillQuaternary};
|
||||
`,
|
||||
copyable: css`
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
margin-block: 2px;
|
||||
padding: 4px;
|
||||
|
||||
&:hover {
|
||||
border-radius: 6px;
|
||||
background: ${token.colorFillTertiary};
|
||||
}
|
||||
`,
|
||||
key: css`
|
||||
color: ${token.colorTextTertiary};
|
||||
`,
|
||||
row: css`
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
&:not(:first-child) {
|
||||
border-block-start: 1px dotted ${token.colorBorderSecondary};
|
||||
}
|
||||
`,
|
||||
shineText: shinyTextStylish(token),
|
||||
value: css`
|
||||
color: ${token.colorTextSecondary};
|
||||
`,
|
||||
}));
|
||||
|
||||
interface ObjectDisplayProps {
|
||||
data: Record<string, any>;
|
||||
shine?: boolean;
|
||||
}
|
||||
|
||||
const ObjectDisplay = memo(({ data, shine }: ObjectDisplayProps) => {
|
||||
const { styles, cx } = useStyles();
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
const { message } = App.useApp();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const formatValue = (value: any): string | string[] => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((v) => (typeof v === 'object' ? JSON.stringify(v) : v));
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return Object.entries(value)
|
||||
.map(([k, v]) => `${k}: ${typeof v === 'object' ? JSON.stringify(v) : v}`)
|
||||
.join(', ');
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const hasMinWidth = Object.keys(data).length > 1;
|
||||
if (Object.keys(data).length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{Object.entries(data).map(([key, value]) => {
|
||||
const formatedValue = formatValue(value);
|
||||
return (
|
||||
<div className={styles.row} key={key}>
|
||||
<span
|
||||
className={styles.key}
|
||||
style={{ minWidth: hasMinWidth ? (isMobile ? 60 : 80) : undefined }}
|
||||
>
|
||||
{key}
|
||||
</span>
|
||||
<span className={styles.colon}>:</span>
|
||||
<div className={cx(shine ? styles.shineText : styles.value)} style={{ width: '100%' }}>
|
||||
{typeof formatedValue === 'string' ? (
|
||||
<div
|
||||
className={styles.copyable}
|
||||
onClick={async () => {
|
||||
await copyToClipboard(formatedValue);
|
||||
message.success(t('copySuccess'));
|
||||
}}
|
||||
>
|
||||
{formatedValue}
|
||||
</div>
|
||||
) : (
|
||||
formatedValue.map((v, i) => (
|
||||
<div
|
||||
className={styles.arrayRow}
|
||||
key={i}
|
||||
onClick={async () => {
|
||||
await copyToClipboard(v);
|
||||
message.success(t('copySuccess'));
|
||||
}}
|
||||
>
|
||||
<div className={styles.copyable}>{v}</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export interface ArgumentsProps {
|
||||
arguments?: string;
|
||||
shine?: boolean;
|
||||
}
|
||||
|
||||
const Arguments = memo<ArgumentsProps>(({ arguments: args = '', shine }) => {
|
||||
const requestArgs = useMemo(() => {
|
||||
try {
|
||||
const obj = parse(args);
|
||||
|
||||
if (Object.keys(obj).length === 0) return {};
|
||||
|
||||
return obj;
|
||||
} catch {
|
||||
return args;
|
||||
}
|
||||
}, [args]);
|
||||
|
||||
const yaml = useYamlArguments(args);
|
||||
|
||||
return typeof requestArgs === 'string' ? (
|
||||
!!yaml && (
|
||||
<Highlighter language={'yaml'} showLanguage={false}>
|
||||
{yaml}
|
||||
</Highlighter>
|
||||
)
|
||||
) : (
|
||||
<ObjectDisplay data={requestArgs} shine={shine} />
|
||||
);
|
||||
});
|
||||
|
||||
export default Arguments;
|
||||
+34
-35
@@ -2,7 +2,7 @@ import { Icon } from '@lobehub/ui';
|
||||
import { ConfigProvider, Empty } from 'antd';
|
||||
import { useTheme } from 'antd-style';
|
||||
import { LucideSquareArrowLeft, LucideSquareArrowRight } from 'lucide-react';
|
||||
import { memo, useContext, useState } from 'react';
|
||||
import { memo, useContext, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Center, Flexbox } from 'react-layout-kit';
|
||||
|
||||
@@ -11,14 +11,15 @@ import { useChatStore } from '@/store/chat';
|
||||
import { chatPortalSelectors, chatSelectors } from '@/store/chat/selectors';
|
||||
import { ChatMessage } from '@/types/message';
|
||||
|
||||
import Arguments from '../../components/Arguments';
|
||||
import Inspector from './Inspector';
|
||||
import Arguments from './Arguments';
|
||||
|
||||
const Tool = memo<
|
||||
const CustomRender = memo<
|
||||
ChatMessage & {
|
||||
showPortal?: boolean;
|
||||
requestArgs?: string;
|
||||
setShowPluginRender: (show: boolean) => void;
|
||||
showPluginRender: boolean;
|
||||
}
|
||||
>(({ id, content, pluginState, plugin, showPortal }) => {
|
||||
>(({ id, content, pluginState, plugin, requestArgs, showPluginRender, setShowPluginRender }) => {
|
||||
const [loading, isMessageToolUIOpen] = useChatStore((s) => [
|
||||
chatSelectors.isPluginApiInvoking(id)(s),
|
||||
chatPortalSelectors.isPluginUIOpen(id)(s),
|
||||
@@ -27,36 +28,34 @@ const Tool = memo<
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const theme = useTheme();
|
||||
const [showRender, setShow] = useState(plugin?.type !== 'default');
|
||||
useEffect(() => {
|
||||
if (!plugin?.type) return;
|
||||
|
||||
setShowPluginRender(plugin?.type !== 'default');
|
||||
}, [plugin?.type]);
|
||||
|
||||
if (isMessageToolUIOpen)
|
||||
return (
|
||||
<Center paddingBlock={8} style={{ background: theme.colorFillQuaternary, borderRadius: 4 }}>
|
||||
<Empty
|
||||
description={t('showInPortal')}
|
||||
image={
|
||||
<Icon
|
||||
color={theme.colorTextQuaternary}
|
||||
icon={direction === 'rtl' ? LucideSquareArrowLeft : LucideSquareArrowRight}
|
||||
size={'large'}
|
||||
/>
|
||||
}
|
||||
styles={{
|
||||
image: { height: 24 },
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
);
|
||||
|
||||
return (
|
||||
<Flexbox gap={12} id={id} width={'100%'}>
|
||||
<Inspector
|
||||
arguments={plugin?.arguments}
|
||||
content={content}
|
||||
id={id}
|
||||
identifier={plugin?.identifier}
|
||||
loading={loading}
|
||||
payload={plugin}
|
||||
setShow={setShow}
|
||||
showPortal={showPortal}
|
||||
showRender={showRender}
|
||||
/>
|
||||
{isMessageToolUIOpen ? (
|
||||
<Center paddingBlock={8} style={{ background: theme.colorFillQuaternary, borderRadius: 4 }}>
|
||||
<Empty
|
||||
description={t('showInPortal')}
|
||||
image={
|
||||
<Icon
|
||||
color={theme.colorTextQuaternary}
|
||||
icon={direction === 'rtl' ? LucideSquareArrowLeft : LucideSquareArrowRight}
|
||||
size={'large'}
|
||||
/>
|
||||
}
|
||||
imageStyle={{ height: 24 }}
|
||||
/>
|
||||
</Center>
|
||||
) : showRender || loading ? (
|
||||
{showPluginRender ? (
|
||||
<PluginRender
|
||||
arguments={plugin?.arguments}
|
||||
content={content}
|
||||
@@ -68,10 +67,10 @@ const Tool = memo<
|
||||
type={plugin?.type}
|
||||
/>
|
||||
) : (
|
||||
<Arguments arguments={plugin?.arguments} />
|
||||
<Arguments arguments={requestArgs} />
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default Tool;
|
||||
export default CustomRender;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Suspense, memo } from 'react';
|
||||
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { chatSelectors } from '@/store/chat/selectors';
|
||||
|
||||
import Arguments from './Arguments';
|
||||
import CustomRender from './CustomRender';
|
||||
|
||||
interface RenderProps {
|
||||
messageId: string;
|
||||
requestArgs?: string;
|
||||
setShowPluginRender: (show: boolean) => void;
|
||||
showPluginRender: boolean;
|
||||
toolCallId: string;
|
||||
toolIndex: number;
|
||||
}
|
||||
const Render = memo<RenderProps>(
|
||||
({ toolCallId, toolIndex, messageId, requestArgs, showPluginRender, setShowPluginRender }) => {
|
||||
const loading = useChatStore(chatSelectors.isToolCallStreaming(messageId, toolIndex));
|
||||
const toolMessage = useChatStore(chatSelectors.getMessageByToolCallId(toolCallId));
|
||||
|
||||
// 如果处于 loading 或者找不到 toolMessage 则展示 Arguments
|
||||
if (loading || !toolMessage) return <Arguments arguments={requestArgs} />;
|
||||
|
||||
if (!!toolMessage)
|
||||
return (
|
||||
<Suspense fallback={<Arguments arguments={requestArgs} shine />}>
|
||||
<CustomRender
|
||||
{...toolMessage}
|
||||
requestArgs={requestArgs}
|
||||
setShowPluginRender={setShowPluginRender}
|
||||
showPluginRender={showPluginRender}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default Render;
|
||||
@@ -0,0 +1,70 @@
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { CSSProperties, memo, useState } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import Inspectors from './Inspector';
|
||||
import Render from './Render';
|
||||
|
||||
export interface InspectorProps {
|
||||
apiName: string;
|
||||
arguments?: string;
|
||||
id: string;
|
||||
identifier: string;
|
||||
index: number;
|
||||
messageId: string;
|
||||
payload: object;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
const Tool = memo<InspectorProps>(
|
||||
({ arguments: requestArgs, apiName, messageId, id, index, identifier, style, payload }) => {
|
||||
const [showRender, setShowRender] = useState(true);
|
||||
const [showPluginRender, setShowPluginRender] = useState(false);
|
||||
|
||||
return (
|
||||
<Flexbox gap={8} style={style}>
|
||||
<Inspectors
|
||||
apiName={apiName}
|
||||
arguments={requestArgs}
|
||||
id={id}
|
||||
identifier={identifier}
|
||||
index={index}
|
||||
messageId={messageId}
|
||||
payload={payload}
|
||||
setShowPluginRender={setShowPluginRender}
|
||||
setShowRender={setShowRender}
|
||||
showPluginRender={showPluginRender}
|
||||
showRender={showRender}
|
||||
/>
|
||||
<AnimatePresence initial={false}>
|
||||
{showRender && (
|
||||
<motion.div
|
||||
animate="open"
|
||||
exit="collapsed"
|
||||
initial="collapsed"
|
||||
transition={{
|
||||
duration: 0.1,
|
||||
ease: [0.4, 0, 0.2, 1], // 使用 ease-out 缓动函数
|
||||
}}
|
||||
variants={{
|
||||
collapsed: { height: 0, opacity: 0, width: 0 },
|
||||
open: { height: 'auto', opacity: 1, width: 'auto' },
|
||||
}}
|
||||
>
|
||||
<Render
|
||||
messageId={messageId}
|
||||
requestArgs={requestArgs}
|
||||
setShowPluginRender={setShowPluginRender}
|
||||
showPluginRender={showPluginRender}
|
||||
toolCallId={id}
|
||||
toolIndex={index}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default Tool;
|
||||
@@ -1,166 +0,0 @@
|
||||
import { Loading3QuartersOutlined } from '@ant-design/icons';
|
||||
import { ActionIcon, Highlighter, Icon, Tag } from '@lobehub/ui';
|
||||
import { Tabs, Typography } from 'antd';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import {
|
||||
BetweenVerticalStart,
|
||||
LucideBug,
|
||||
LucideBugOff,
|
||||
LucideChevronDown,
|
||||
LucideChevronRight,
|
||||
} from 'lucide-react';
|
||||
import { memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { DESKTOP_HEADER_ICON_SIZE } from '@/const/layoutTokens';
|
||||
import PluginAvatar from '@/features/PluginAvatar';
|
||||
import { useIsMobile } from '@/hooks/useIsMobile';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { chatPortalSelectors } from '@/store/chat/selectors';
|
||||
import { pluginHelpers, useToolStore } from '@/store/tool';
|
||||
import { toolSelectors } from '@/store/tool/selectors';
|
||||
import { ChatPluginPayload } from '@/types/message';
|
||||
|
||||
import PluginResult from './PluginResultJSON';
|
||||
import Settings from './Settings';
|
||||
import { useStyles } from './style';
|
||||
|
||||
export interface InspectorProps {
|
||||
arguments?: string;
|
||||
content: string;
|
||||
id: string;
|
||||
identifier?: string;
|
||||
loading?: boolean;
|
||||
payload?: ChatPluginPayload;
|
||||
setShow?: (showRender: boolean) => void;
|
||||
showPortal?: boolean;
|
||||
showRender?: boolean;
|
||||
}
|
||||
|
||||
const Inspector = memo<InspectorProps>(
|
||||
({
|
||||
arguments: requestArgs = '{}',
|
||||
payload,
|
||||
showRender,
|
||||
loading,
|
||||
setShow,
|
||||
content,
|
||||
identifier = 'unknown',
|
||||
id,
|
||||
showPortal = true,
|
||||
}) => {
|
||||
const { t } = useTranslation(['plugin', 'portal']);
|
||||
const { styles } = useStyles();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [isMessageToolUIOpen, openToolUI, togglePortal] = useChatStore((s) => [
|
||||
chatPortalSelectors.isPluginUIOpen(id)(s),
|
||||
s.openToolUI,
|
||||
s.togglePortal,
|
||||
]);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const pluginMeta = useToolStore(toolSelectors.getMetaById(identifier), isEqual);
|
||||
|
||||
const showRightAction = useToolStore(toolSelectors.isToolHasUI(identifier));
|
||||
|
||||
const pluginTitle = pluginHelpers.getPluginTitle(pluginMeta) ?? t('unknownPlugin');
|
||||
|
||||
let args, params;
|
||||
try {
|
||||
args = JSON.stringify(payload, null, 2);
|
||||
params = JSON.stringify(JSON.parse(requestArgs), null, 2);
|
||||
} catch {
|
||||
args = '';
|
||||
params = '';
|
||||
}
|
||||
|
||||
return (
|
||||
<Flexbox gap={8}>
|
||||
<Flexbox align={'center'} distribution={'space-between'} gap={24} horizontal>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={styles.container}
|
||||
gap={isMobile ? 16 : 8}
|
||||
horizontal
|
||||
onClick={() => {
|
||||
setShow?.(!showRender);
|
||||
}}
|
||||
>
|
||||
<Flexbox align={'center'} gap={8} horizontal>
|
||||
{loading ? (
|
||||
<div>
|
||||
<Loading3QuartersOutlined spin />
|
||||
</div>
|
||||
) : (
|
||||
<PluginAvatar identifier={identifier} size={isMobile ? 36 : undefined} />
|
||||
)}
|
||||
{isMobile ? (
|
||||
<Flexbox>
|
||||
<div>{pluginTitle}</div>
|
||||
<Typography.Text className={styles.apiName} type={'secondary'}>
|
||||
{payload?.apiName}
|
||||
</Typography.Text>
|
||||
</Flexbox>
|
||||
) : (
|
||||
<>
|
||||
<div>{pluginTitle}</div>
|
||||
<Tag>{payload?.apiName}</Tag>
|
||||
</>
|
||||
)}
|
||||
</Flexbox>
|
||||
{showRightAction && <Icon icon={showRender ? LucideChevronDown : LucideChevronRight} />}
|
||||
</Flexbox>
|
||||
|
||||
<Flexbox horizontal>
|
||||
{!isMobile && showRightAction && showPortal && (
|
||||
<ActionIcon
|
||||
icon={BetweenVerticalStart}
|
||||
onClick={() => {
|
||||
if (!isMessageToolUIOpen) openToolUI(id, identifier);
|
||||
else {
|
||||
togglePortal(false);
|
||||
}
|
||||
}}
|
||||
size={DESKTOP_HEADER_ICON_SIZE}
|
||||
title={t('title', { ns: 'portal' })}
|
||||
/>
|
||||
)}
|
||||
<ActionIcon
|
||||
icon={open ? LucideBugOff : LucideBug}
|
||||
onClick={() => {
|
||||
setOpen(!open);
|
||||
}}
|
||||
title={t(open ? 'debug.off' : 'debug.on')}
|
||||
/>
|
||||
<Settings id={identifier} />
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
{open && (
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
children: <Highlighter language={'json'}>{args}</Highlighter>,
|
||||
key: 'function_call',
|
||||
label: t('debug.function_call'),
|
||||
},
|
||||
{
|
||||
children: <Highlighter language={'json'}>{params}</Highlighter>,
|
||||
key: 'arguments',
|
||||
label: t('debug.arguments'),
|
||||
},
|
||||
{
|
||||
children: <PluginResult content={content} />,
|
||||
key: 'response',
|
||||
label: t('debug.response'),
|
||||
},
|
||||
]}
|
||||
style={{ display: 'grid', maxWidth: 800, minWidth: 400 }}
|
||||
/>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default Inspector;
|
||||
@@ -1,35 +0,0 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
|
||||
export const useStyles = createStyles(({ css, token }) => ({
|
||||
apiName: css`
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
`,
|
||||
container: css`
|
||||
cursor: pointer;
|
||||
|
||||
width: fit-content;
|
||||
padding-block: 6px;
|
||||
padding-inline: 8px;
|
||||
padding-inline-end: 12px;
|
||||
border: 1px solid ${token.colorBorder};
|
||||
border-radius: 8px;
|
||||
|
||||
color: ${token.colorText};
|
||||
|
||||
&:hover {
|
||||
background: ${token.colorFillTertiary};
|
||||
}
|
||||
`,
|
||||
plugin: css`
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
`,
|
||||
}));
|
||||
@@ -1,89 +0,0 @@
|
||||
import { Icon, Tag } from '@lobehub/ui';
|
||||
import { Typography } from 'antd';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { CSSProperties, memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import PluginAvatar from '@/features/PluginAvatar';
|
||||
import { useIsMobile } from '@/hooks/useIsMobile';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { chatSelectors } from '@/store/chat/selectors';
|
||||
import { pluginHelpers, useToolStore } from '@/store/tool';
|
||||
import { toolSelectors } from '@/store/tool/selectors';
|
||||
|
||||
import Arguments from '../../components/Arguments';
|
||||
import ToolMessage from './Tool';
|
||||
import { useStyles } from './style';
|
||||
|
||||
export interface InspectorProps {
|
||||
apiName: string;
|
||||
arguments?: string;
|
||||
id: string;
|
||||
identifier: string;
|
||||
index: number;
|
||||
messageId: string;
|
||||
showPortal?: boolean;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
const CallItem = memo<InspectorProps>(
|
||||
({ arguments: requestArgs, apiName, messageId, id, index, identifier, style, showPortal }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const { styles } = useStyles();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const loading = useChatStore(chatSelectors.isToolCallStreaming(messageId, index));
|
||||
const toolMessage = useChatStore(chatSelectors.getMessageByToolCallId(id));
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const pluginMeta = useToolStore(toolSelectors.getMetaById(identifier), isEqual);
|
||||
|
||||
const pluginTitle = pluginHelpers.getPluginTitle(pluginMeta) ?? t('unknownPlugin');
|
||||
|
||||
// when tool calling stop streaming, we should show the tool message
|
||||
return !loading && toolMessage ? (
|
||||
<ToolMessage {...toolMessage} showPortal={showPortal} />
|
||||
) : (
|
||||
<Flexbox gap={8} style={style}>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={styles.container}
|
||||
distribution={'space-between'}
|
||||
gap={8}
|
||||
horizontal
|
||||
onClick={() => {
|
||||
setOpen(!open);
|
||||
}}
|
||||
>
|
||||
<Flexbox align={'center'} gap={8} horizontal>
|
||||
{loading ? (
|
||||
<div>
|
||||
<Icon icon={Loader2} spin />
|
||||
</div>
|
||||
) : (
|
||||
<PluginAvatar identifier={identifier} size={isMobile ? 36 : undefined} />
|
||||
)}
|
||||
{isMobile ? (
|
||||
<Flexbox>
|
||||
<div>{pluginTitle}</div>
|
||||
<Typography.Text className={styles.apiName} type={'secondary'}>
|
||||
{apiName}
|
||||
</Typography.Text>
|
||||
</Flexbox>
|
||||
) : (
|
||||
<>
|
||||
<div>{pluginTitle}</div>
|
||||
<Tag>{apiName}</Tag>
|
||||
</>
|
||||
)}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
{loading && <Arguments arguments={requestArgs} />}
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default CallItem;
|
||||
@@ -1,35 +0,0 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
|
||||
export const useStyles = createStyles(({ css, token }) => ({
|
||||
apiName: css`
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
`,
|
||||
container: css`
|
||||
cursor: pointer;
|
||||
|
||||
width: fit-content;
|
||||
padding-block: 6px;
|
||||
padding-inline: 8px;
|
||||
padding-inline-end: 12px;
|
||||
border: 1px solid ${token.colorBorder};
|
||||
border-radius: 8px;
|
||||
|
||||
color: ${token.colorText};
|
||||
|
||||
&:hover {
|
||||
background: ${token.colorFillTertiary};
|
||||
}
|
||||
`,
|
||||
plugin: css`
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
`,
|
||||
}));
|
||||
@@ -1,18 +1,15 @@
|
||||
import { Skeleton } from 'antd';
|
||||
import { ReactNode, Suspense, memo, useContext } from 'react';
|
||||
import { ReactNode, memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { LOADING_FLAT } from '@/const/message';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { chatSelectors } from '@/store/chat/selectors';
|
||||
import { aiChatSelectors } from '@/store/chat/slices/aiChat/selectors';
|
||||
import { aiChatSelectors, chatSelectors } from '@/store/chat/selectors';
|
||||
import { ChatMessage } from '@/types/message';
|
||||
|
||||
import { InPortalThreadContext } from '../../components/ChatItem/InPortalThreadContext';
|
||||
import { DefaultMessage } from '../Default';
|
||||
import FileChunks from './FileChunks';
|
||||
import Thinking from './Reasoning';
|
||||
import ToolCall from './ToolCallItem';
|
||||
import Reasoning from './Reasoning';
|
||||
import Tool from './Tool';
|
||||
|
||||
export const AssistantMessage = memo<
|
||||
ChatMessage & {
|
||||
@@ -22,7 +19,6 @@ export const AssistantMessage = memo<
|
||||
const editing = useChatStore(chatSelectors.isMessageEditing(id));
|
||||
const generating = useChatStore(chatSelectors.isMessageGenerating(id));
|
||||
|
||||
const inThread = useContext(InPortalThreadContext);
|
||||
const isToolCallGenerating = generating && (content === LOADING_FLAT || !content) && !!tools;
|
||||
|
||||
const isReasoning = useChatStore(aiChatSelectors.isMessageInReasoning(id));
|
||||
@@ -43,7 +39,7 @@ export const AssistantMessage = memo<
|
||||
) : (
|
||||
<Flexbox gap={8} id={id}>
|
||||
{!!chunksList && chunksList.length > 0 && <FileChunks data={chunksList} />}
|
||||
{showReasoning && <Thinking {...props.reasoning} id={id} />}
|
||||
{showReasoning && <Reasoning {...props.reasoning} id={id} />}
|
||||
{content && (
|
||||
<DefaultMessage
|
||||
addIdOnDOM={false}
|
||||
@@ -54,24 +50,20 @@ export const AssistantMessage = memo<
|
||||
/>
|
||||
)}
|
||||
{tools && (
|
||||
<Suspense
|
||||
fallback={<Skeleton.Button active style={{ height: 46, minWidth: 200, width: '100%' }} />}
|
||||
>
|
||||
<Flexbox gap={8}>
|
||||
{tools.map((toolCall, index) => (
|
||||
<ToolCall
|
||||
apiName={toolCall.apiName}
|
||||
arguments={toolCall.arguments}
|
||||
id={toolCall.id}
|
||||
identifier={toolCall.identifier}
|
||||
index={index}
|
||||
key={toolCall.id}
|
||||
messageId={id}
|
||||
showPortal={!inThread}
|
||||
/>
|
||||
))}
|
||||
</Flexbox>
|
||||
</Suspense>
|
||||
<Flexbox gap={8}>
|
||||
{tools.map((toolCall, index) => (
|
||||
<Tool
|
||||
apiName={toolCall.apiName}
|
||||
arguments={toolCall.arguments}
|
||||
id={toolCall.id}
|
||||
identifier={toolCall.identifier}
|
||||
index={index}
|
||||
key={toolCall.id}
|
||||
messageId={id}
|
||||
payload={toolCall}
|
||||
/>
|
||||
))}
|
||||
</Flexbox>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Highlighter } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useYamlArguments } from '@/hooks/useYamlArguments';
|
||||
|
||||
export interface ArgumentsProps {
|
||||
arguments?: string;
|
||||
}
|
||||
|
||||
const Arguments = memo<ArgumentsProps>(({ arguments: args = '' }) => {
|
||||
const yaml = useYamlArguments(args);
|
||||
|
||||
return (
|
||||
!!yaml && (
|
||||
<Highlighter language={'yaml'} showLanguage={false}>
|
||||
{yaml}
|
||||
</Highlighter>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
export default Arguments;
|
||||
@@ -17,9 +17,9 @@ const PGliteIcon: IconType = forwardRef(({ size = '1em', style }, ref) => {
|
||||
>
|
||||
<title>PGlite</title>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M941.581 335.737v460.806c0 15.926-12.913 28.836-28.832 28.818l-115.283-.137c-15.243-.018-27.706-11.88-28.703-26.877.011-.569.018-1.138.018-1.711l-.004-172.904c0-47.745-38.736-86.451-86.454-86.451-46.245 0-84.052-36.359-86.342-82.068V191.496l201.708.149c79.484.058 143.892 64.553 143.892 144.092zm-576-144.281v201.818c0 47.746 38.682 86.456 86.4 86.456h86.4v-5.796c0 66.816 54.13 120.98 120.902 120.98 28.617 0 51.815 23.213 51.815 51.848v149.644c0 .688.011 1.372.025 2.057-.943 15.065-13.453 26.992-28.746 26.992l-144.982-.007.986-201.586c.079-15.915-12.755-28.88-28.66-28.959-15.904-.079-28.861 12.763-28.94 28.678l-.986 201.741v.118l-172.174-.01V623.722c0-15.915-12.895-28.819-28.8-28.819-15.906 0-28.8 12.904-28.8 28.819v201.704l-143.642-.007c-15.905-.004-28.798-12.904-28.798-28.819V335.547c0-79.58 64.471-144.093 144.001-144.092l143.999.001zm446.544 173.693c0-23.874-19.343-43.228-43.2-43.228-23.861 0-43.2 19.354-43.2 43.228 0 23.875 19.339 43.226 43.2 43.226 23.857 0 43.2-19.351 43.2-43.226z"
|
||||
fill-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
import { PluginRequestPayload } from '@lobehub/chat-plugin-sdk';
|
||||
import { Skeleton } from 'antd';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { LobeToolRenderType } from '@/types/tool';
|
||||
|
||||
import BuiltinType from './BuiltinType';
|
||||
import DefaultType from './DefaultType';
|
||||
import Markdown from './MarkdownType';
|
||||
|
||||
const loading = () => (
|
||||
<Skeleton.Node active style={{ width: '100%' }}>
|
||||
{' '}
|
||||
</Skeleton.Node>
|
||||
);
|
||||
|
||||
const Standalone = dynamic(() => import('./StandaloneType'), { loading });
|
||||
const BuiltinType = dynamic(() => import('./BuiltinType'), { loading });
|
||||
import Standalone from './StandaloneType';
|
||||
|
||||
export interface PluginRenderProps {
|
||||
arguments?: string;
|
||||
|
||||
@@ -119,6 +119,10 @@ export default {
|
||||
reinstallError: '插件 {{name}} 刷新失败',
|
||||
urlError: '该链接没有返回 JSON 格式的内容, 请确保是有效的链接',
|
||||
},
|
||||
inspector: {
|
||||
args: '查看参数列表',
|
||||
pluginRender: '查看插件界面',
|
||||
},
|
||||
list: {
|
||||
item: {
|
||||
'deprecated.title': '已删除',
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { css } from 'antd-style';
|
||||
import type { FullToken } from 'antd-style/lib/types';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
export const dotLoading = css`
|
||||
&::after {
|
||||
@@ -26,3 +28,28 @@ export const dotLoading = css`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const shinyTextStylish = (token: FullToken) => css`
|
||||
color: ${rgba(token.colorText, 0.45)};
|
||||
|
||||
background: linear-gradient(
|
||||
120deg,
|
||||
${rgba(token.colorTextBase, 0)} 40%,
|
||||
${token.colorTextSecondary} 50%,
|
||||
${rgba(token.colorTextBase, 0)} 60%
|
||||
);
|
||||
background-clip: text;
|
||||
background-size: 200% 100%;
|
||||
|
||||
animation: shine 1.5s linear infinite;
|
||||
|
||||
@keyframes shine {
|
||||
0% {
|
||||
background-position: 100%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -100%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useResponsive } from 'antd-style';
|
||||
import { ReactNode, memo, useMemo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import Grid from '@/components/GalleyGrid/Grid';
|
||||
|
||||
const MAX_SIZE_DESKTOP = 640;
|
||||
const MAX_SIZE_MOBILE = 280;
|
||||
|
||||
interface GalleyGridProps<T = any> {
|
||||
items: T[];
|
||||
renderItem: (props: T) => ReactNode;
|
||||
}
|
||||
|
||||
const GalleyGrid = memo<GalleyGridProps>(({ items, renderItem: Render }) => {
|
||||
const { mobile } = useResponsive();
|
||||
|
||||
const { firstRow, lastRow } = useMemo(() => {
|
||||
if (items.length === 4) {
|
||||
return {
|
||||
firstRow: items.slice(0, 2),
|
||||
lastRow: items.slice(2, 4),
|
||||
};
|
||||
}
|
||||
|
||||
const firstCol = items.length % 3 === 0 ? 3 : items.length % 3;
|
||||
|
||||
return {
|
||||
firstRow: items.slice(0, firstCol),
|
||||
lastRow: items.slice(firstCol, items.length),
|
||||
};
|
||||
}, [items]);
|
||||
|
||||
const { gap, max } = useMemo(
|
||||
() => ({
|
||||
gap: mobile ? 4 : 6,
|
||||
max: (mobile ? MAX_SIZE_MOBILE : MAX_SIZE_DESKTOP) * firstRow.length,
|
||||
}),
|
||||
[mobile],
|
||||
);
|
||||
|
||||
return (
|
||||
<Flexbox gap={gap}>
|
||||
<Grid col={firstRow.length} gap={gap} max={max}>
|
||||
{firstRow.map((i, index) => (
|
||||
<Render {...i} index={index} key={index} />
|
||||
))}
|
||||
</Grid>
|
||||
{lastRow.length > 0 && (
|
||||
<Grid col={lastRow.length > 2 ? 3 : lastRow.length} gap={gap} max={max}>
|
||||
{lastRow.map((i, index) => (
|
||||
<Render {...i} index={index} key={index} />
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default GalleyGrid;
|
||||
@@ -3,11 +3,11 @@ import { Download } from 'lucide-react';
|
||||
import { memo, useRef } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import GalleyGrid from '@/components/GalleyGrid';
|
||||
import { fileService } from '@/services/file';
|
||||
import { BuiltinRenderProps } from '@/types/tool';
|
||||
import { DallEImageItem } from '@/types/tool/dalle';
|
||||
|
||||
import GalleyGrid from './GalleyGrid';
|
||||
import ImageItem from './Item';
|
||||
|
||||
const DallE = memo<BuiltinRenderProps<DallEImageItem[]>>(({ content, messageId }) => {
|
||||
|
||||
Reference in New Issue
Block a user