mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-05 05:16:03 +00:00
feat: add emoji support to knowledge bases and pipelines (#1935)
* feat: add emoji support to knowledge bases and pipelines * feat: add optional emoji property to ExternalKBCardVO for enhanced knowledge base representation
This commit is contained in:
committed by
GitHub
parent
d6e1e79f07
commit
b73847f1a6
@@ -11,6 +11,7 @@ class LegacyPipeline(Base):
|
||||
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
|
||||
name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
||||
description = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
||||
emoji = sqlalchemy.Column(sqlalchemy.String(10), nullable=True, default='⚙️')
|
||||
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
|
||||
updated_at = sqlalchemy.Column(
|
||||
sqlalchemy.DateTime,
|
||||
|
||||
@@ -7,6 +7,7 @@ class KnowledgeBase(Base):
|
||||
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
|
||||
name = sqlalchemy.Column(sqlalchemy.String, index=True)
|
||||
description = sqlalchemy.Column(sqlalchemy.Text)
|
||||
emoji = sqlalchemy.Column(sqlalchemy.String(10), nullable=True, default='📚')
|
||||
created_at = sqlalchemy.Column(sqlalchemy.DateTime, default=sqlalchemy.func.now())
|
||||
updated_at = sqlalchemy.Column(sqlalchemy.DateTime, default=sqlalchemy.func.now(), onupdate=sqlalchemy.func.now())
|
||||
embedding_model_uuid = sqlalchemy.Column(sqlalchemy.String, default='')
|
||||
@@ -35,6 +36,7 @@ class ExternalKnowledgeBase(Base):
|
||||
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
|
||||
name = sqlalchemy.Column(sqlalchemy.String, index=True)
|
||||
description = sqlalchemy.Column(sqlalchemy.Text)
|
||||
emoji = sqlalchemy.Column(sqlalchemy.String(10), nullable=True, default='🔗')
|
||||
plugin_author = sqlalchemy.Column(sqlalchemy.String, nullable=False)
|
||||
plugin_name = sqlalchemy.Column(sqlalchemy.String, nullable=False)
|
||||
retriever_name = sqlalchemy.Column(sqlalchemy.String, nullable=False)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import sqlalchemy
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class(18)
|
||||
class DBMigrateAddEmojiSupport(migration.DBMigration):
|
||||
"""Add emoji field to knowledge_bases, external_knowledge_bases and legacy_pipelines tables"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
# Add emoji field to knowledge_bases
|
||||
await self._add_emoji_to_table('knowledge_bases', '📚')
|
||||
|
||||
# Add emoji field to external_knowledge_bases
|
||||
await self._add_emoji_to_table('external_knowledge_bases', '🔗')
|
||||
|
||||
# Add emoji field to legacy_pipelines
|
||||
await self._add_emoji_to_table('legacy_pipelines', '⚙️')
|
||||
|
||||
async def _add_emoji_to_table(self, table_name: str, default_emoji: str):
|
||||
"""Add emoji column to specified table if it doesn't exist"""
|
||||
# Get all column names from the table
|
||||
columns = []
|
||||
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
f"SELECT column_name FROM information_schema.columns WHERE table_name = '{table_name}';"
|
||||
)
|
||||
)
|
||||
all_result = result.fetchall()
|
||||
columns = [row[0] for row in all_result]
|
||||
else:
|
||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.text(f'PRAGMA table_info({table_name});'))
|
||||
all_result = result.fetchall()
|
||||
columns = [row[1] for row in all_result]
|
||||
|
||||
# Check and add emoji column
|
||||
if 'emoji' not in columns:
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(f"ALTER TABLE {table_name} ADD COLUMN emoji VARCHAR(10) DEFAULT '{default_emoji}'")
|
||||
)
|
||||
else:
|
||||
# SQLite doesn't support DEFAULT with emoji directly in ALTER TABLE
|
||||
# Add column without default first
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(f'ALTER TABLE {table_name} ADD COLUMN emoji VARCHAR(10)')
|
||||
)
|
||||
|
||||
# Set default emoji value for existing records
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(f"UPDATE {table_name} SET emoji = '{default_emoji}' WHERE emoji IS NULL")
|
||||
)
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
pass
|
||||
@@ -2,7 +2,7 @@ import langbot
|
||||
|
||||
semantic_version = f'v{langbot.__version__}'
|
||||
|
||||
required_database_version = 17
|
||||
required_database_version = 18
|
||||
"""Tag the version of the database schema, used to check if the database needs to be migrated"""
|
||||
|
||||
debug_mode = False
|
||||
|
||||
4169
web/pnpm-lock.yaml
generated
4169
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -11,61 +11,46 @@ export default function ExternalKBCard({
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className={`${styles.cardContainer}`}>
|
||||
<div className="w-full h-full flex flex-row items-start gap-3">
|
||||
{/* Icon */}
|
||||
<img
|
||||
src={httpClient.getPluginIconURL(
|
||||
kbCardVO.pluginAuthor,
|
||||
kbCardVO.pluginName,
|
||||
)}
|
||||
alt="plugin icon"
|
||||
className="w-16 h-16 mt-1 rounded-[8%] flex-shrink-0"
|
||||
/>
|
||||
|
||||
{/* Info Column */}
|
||||
<div className="flex flex-col flex-1 min-w-0 h-full">
|
||||
{/* Top section: Name, Description and Plugin Info */}
|
||||
<div className="flex flex-col gap-0">
|
||||
{/* Name and Description */}
|
||||
<div className={`${styles.basicInfoNameContainer}`}>
|
||||
<div className={`${styles.basicInfoNameText} ${styles.bigText}`}>
|
||||
{kbCardVO.name}
|
||||
</div>
|
||||
<div className={`${styles.basicInfoDescriptionText}`}>
|
||||
{kbCardVO.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plugin Info */}
|
||||
<div className="flex flex-row gap-2 items-center mt-1">
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M7 5C7 2.79086 8.79086 1 11 1C13.2091 1 15 2.79086 15 5H18C18.5523 5 19 5.44772 19 6V9C21.2091 9 23 10.7909 23 13C23 15.2091 21.2091 17 19 17V20C19 20.5523 18.5523 21 18 21H4C3.44772 21 3 20.5523 3 20V6C3 5.44772 3.44772 5 4 5H7ZM11 3C9.89543 3 9 3.89543 9 5C9 5.23554 9.0403 5.45952 9.11355 5.66675C9.22172 5.97282 9.17461 6.31235 8.98718 6.57739C8.79974 6.84243 8.49532 7 8.17071 7H5V19H17V15.8293C17 15.5047 17.1576 15.2003 17.4226 15.0128C17.6877 14.8254 18.0272 14.7783 18.3332 14.8865C18.5405 14.9597 18.7645 15 19 15C20.1046 15 21 14.1046 21 13C21 11.8954 20.1046 11 19 11C18.7645 11 18.5405 11.0403 18.3332 11.1135C18.0272 11.2217 17.6877 11.1746 17.4226 10.9872C17.1576 10.7997 17 10.4953 17 10.1707V7H13.8293C13.5047 7 13.2003 6.84243 13.0128 6.57739C12.8254 6.31235 12.7783 5.97282 12.8865 5.66675C12.9597 5.45952 13 5.23555 13 5C13 3.89543 12.1046 3 11 3Z"></path>
|
||||
</svg>
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
{kbCardVO.pluginAuthor} / {kbCardVO.pluginName}
|
||||
</span>
|
||||
<div className={`${styles.basicInfoContainer}`}>
|
||||
<div className={`${styles.iconBasicInfoContainer}`}>
|
||||
{/* Emoji with plugin icon badge */}
|
||||
<div className="relative">
|
||||
<div className={`${styles.iconEmoji}`}>
|
||||
{kbCardVO.emoji || '🔗'}
|
||||
</div>
|
||||
{/* Plugin icon badge at bottom right */}
|
||||
<img
|
||||
src={httpClient.getPluginIconURL(
|
||||
kbCardVO.pluginAuthor,
|
||||
kbCardVO.pluginName,
|
||||
)}
|
||||
alt="plugin icon"
|
||||
className="absolute -bottom-1 -right-1 w-5 h-5 rounded-[20%]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bottom section: Update Time */}
|
||||
<div className="flex flex-row gap-2 items-center mt-auto">
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM13 12H17V14H11V7H13V12Z"></path>
|
||||
</svg>
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
{t('knowledge.updateTime')}
|
||||
{kbCardVO.lastUpdatedTimeAgo}
|
||||
</span>
|
||||
<div className={`${styles.basicInfoNameContainer}`}>
|
||||
<div className={`${styles.basicInfoNameText} ${styles.bigText}`}>
|
||||
{kbCardVO.name}
|
||||
</div>
|
||||
<div className={`${styles.basicInfoDescriptionText}`}>
|
||||
{kbCardVO.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.basicInfoLastUpdatedTimeContainer}`}>
|
||||
<svg
|
||||
className={`${styles.basicInfoUpdateTimeIcon}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM13 12H17V14H11V7H13V12Z"></path>
|
||||
</svg>
|
||||
<div className={`${styles.basicInfoUpdateTimeText}`}>
|
||||
{t('knowledge.updateTime')}
|
||||
{kbCardVO.lastUpdatedTimeAgo}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ export class ExternalKBCardVO {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
emoji?: string;
|
||||
retrieverName: string;
|
||||
retrieverConfig: Record<string, unknown>;
|
||||
lastUpdatedTimeAgo: string;
|
||||
@@ -12,6 +13,7 @@ export class ExternalKBCardVO {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
emoji,
|
||||
retrieverName,
|
||||
retrieverConfig,
|
||||
lastUpdatedTimeAgo,
|
||||
@@ -21,6 +23,7 @@ export class ExternalKBCardVO {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
emoji?: string;
|
||||
retrieverName: string;
|
||||
retrieverConfig: Record<string, unknown>;
|
||||
lastUpdatedTimeAgo: string;
|
||||
@@ -30,6 +33,7 @@ export class ExternalKBCardVO {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.emoji = emoji;
|
||||
this.retrieverName = retrieverName;
|
||||
this.retrieverConfig = retrieverConfig;
|
||||
this.lastUpdatedTimeAgo = lastUpdatedTimeAgo;
|
||||
|
||||
@@ -15,6 +15,7 @@ import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
|
||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { ExternalKnowledgeBase } from '@/app/infra/entities/api';
|
||||
import EmojiPicker from '@/components/ui/emoji-picker';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -54,6 +55,7 @@ const getFormSchema = (t: (key: string) => string) =>
|
||||
z.object({
|
||||
name: z.string().min(1, { message: t('knowledge.nameRequired') }),
|
||||
description: z.string().optional(),
|
||||
emoji: z.string().optional(),
|
||||
plugin_author: z.string().min(1, { message: 'Please select a retriever' }),
|
||||
plugin_name: z.string().min(1, { message: 'Please select a retriever' }),
|
||||
retriever_name: z.string().min(1, { message: 'Please select a retriever' }),
|
||||
@@ -101,6 +103,7 @@ export default function ExternalKBForm({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
description: '',
|
||||
emoji: '🔗',
|
||||
plugin_author: '',
|
||||
plugin_name: '',
|
||||
retriever_name: '',
|
||||
@@ -140,6 +143,7 @@ export default function ExternalKBForm({
|
||||
// Set form values
|
||||
form.setValue('name', kbConfig.name);
|
||||
form.setValue('description', kbConfig.description || '');
|
||||
form.setValue('emoji', kbConfig.emoji || '🔗');
|
||||
form.setValue('plugin_author', kbConfig.plugin_author);
|
||||
form.setValue('plugin_name', kbConfig.plugin_name);
|
||||
form.setValue('retriever_name', kbConfig.retriever_name);
|
||||
@@ -207,6 +211,7 @@ export default function ExternalKBForm({
|
||||
return {
|
||||
name: kb.name,
|
||||
description: kb.description,
|
||||
emoji: kb.emoji || '🔗',
|
||||
plugin_author: kb.plugin_author,
|
||||
plugin_name: kb.plugin_name,
|
||||
retriever_name: kb.retriever_name,
|
||||
@@ -276,6 +281,7 @@ export default function ExternalKBForm({
|
||||
const formData: ExternalKnowledgeBase = {
|
||||
name: form.getValues().name,
|
||||
description: form.getValues().description || '',
|
||||
emoji: form.getValues().emoji,
|
||||
plugin_author: form.getValues().plugin_author,
|
||||
plugin_name: form.getValues().plugin_name,
|
||||
retriever_name: form.getValues().retriever_name,
|
||||
@@ -390,23 +396,41 @@ export default function ExternalKBForm({
|
||||
className="space-y-8"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* KB Name */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('knowledge.kbName')}
|
||||
<span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* KB Name and Emoji in same row */}
|
||||
<div className="flex gap-4 items-start">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
{t('knowledge.kbName')}
|
||||
<span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="emoji"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('common.icon')}</FormLabel>
|
||||
<FormControl>
|
||||
<EmojiPicker
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* KB Description */}
|
||||
<FormField
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 2px 2px 0 rgba(0, 0, 0, 0.2);
|
||||
padding: 1.2rem;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -32,14 +32,41 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 0.4rem;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.iconEmoji {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:global(.dark) .iconEmoji {
|
||||
background-color: #2a2a2d;
|
||||
}
|
||||
|
||||
.iconBasicInfoContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.basicInfoNameContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.basicInfoNameText {
|
||||
|
||||
@@ -7,12 +7,15 @@ export default function KBCard({ kbCardVO }: { kbCardVO: KnowledgeBaseVO }) {
|
||||
return (
|
||||
<div className={`${styles.cardContainer}`}>
|
||||
<div className={`${styles.basicInfoContainer}`}>
|
||||
<div className={`${styles.basicInfoNameContainer}`}>
|
||||
<div className={`${styles.basicInfoNameText} ${styles.bigText}`}>
|
||||
{kbCardVO.name}
|
||||
</div>
|
||||
<div className={`${styles.basicInfoDescriptionText}`}>
|
||||
{kbCardVO.description}
|
||||
<div className={`${styles.iconBasicInfoContainer}`}>
|
||||
<div className={`${styles.iconEmoji}`}>{kbCardVO.emoji || '📚'}</div>
|
||||
<div className={`${styles.basicInfoNameContainer}`}>
|
||||
<div className={`${styles.basicInfoNameText} ${styles.bigText}`}>
|
||||
{kbCardVO.name}
|
||||
</div>
|
||||
<div className={`${styles.basicInfoDescriptionText}`}>
|
||||
{kbCardVO.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface IKnowledgeBaseVO {
|
||||
embeddingModelUUID: string;
|
||||
top_k: number;
|
||||
lastUpdatedTimeAgo: string;
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
export class KnowledgeBaseVO implements IKnowledgeBaseVO {
|
||||
@@ -14,6 +15,7 @@ export class KnowledgeBaseVO implements IKnowledgeBaseVO {
|
||||
embeddingModelUUID: string;
|
||||
top_k: number;
|
||||
lastUpdatedTimeAgo: string;
|
||||
emoji?: string;
|
||||
|
||||
constructor(props: IKnowledgeBaseVO) {
|
||||
this.id = props.id;
|
||||
@@ -22,5 +24,6 @@ export class KnowledgeBaseVO implements IKnowledgeBaseVO {
|
||||
this.embeddingModelUUID = props.embeddingModelUUID;
|
||||
this.top_k = props.top_k;
|
||||
this.lastUpdatedTimeAgo = props.lastUpdatedTimeAgo;
|
||||
this.emoji = props.emoji;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import EmojiPicker from '@/components/ui/emoji-picker';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -32,6 +33,7 @@ const getFormSchema = (t: (key: string) => string) =>
|
||||
description: z
|
||||
.string()
|
||||
.min(1, { message: t('knowledge.kbDescriptionRequired') }),
|
||||
emoji: z.string().optional(),
|
||||
embeddingModelUUID: z
|
||||
.string()
|
||||
.min(1, { message: t('knowledge.embeddingModelUUIDRequired') }),
|
||||
@@ -58,6 +60,7 @@ export default function KBForm({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
description: t('knowledge.defaultDescription'),
|
||||
emoji: '📚',
|
||||
embeddingModelUUID: '',
|
||||
top_k: 5,
|
||||
},
|
||||
@@ -71,6 +74,7 @@ export default function KBForm({
|
||||
getKbConfig(initKbId).then((val) => {
|
||||
form.setValue('name', val.name);
|
||||
form.setValue('description', val.description);
|
||||
form.setValue('emoji', val.emoji);
|
||||
form.setValue('embeddingModelUUID', val.embeddingModelUUID);
|
||||
form.setValue('top_k', val.top_k || 5);
|
||||
});
|
||||
@@ -86,6 +90,7 @@ export default function KBForm({
|
||||
resolve({
|
||||
name: res.base.name,
|
||||
description: res.base.description,
|
||||
emoji: res.base.emoji || '📚',
|
||||
embeddingModelUUID: res.base.embedding_model_uuid,
|
||||
top_k: res.base.top_k || 5,
|
||||
});
|
||||
@@ -111,6 +116,7 @@ export default function KBForm({
|
||||
const updateKb: KnowledgeBase = {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
emoji: data.emoji,
|
||||
embedding_model_uuid: data.embeddingModelUUID,
|
||||
top_k: data.top_k,
|
||||
};
|
||||
@@ -129,6 +135,7 @@ export default function KBForm({
|
||||
const newKb: KnowledgeBase = {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
emoji: data.emoji,
|
||||
embedding_model_uuid: data.embeddingModelUUID,
|
||||
top_k: data.top_k,
|
||||
};
|
||||
@@ -152,22 +159,41 @@ export default function KBForm({
|
||||
className="space-y-8"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('knowledge.kbName')}
|
||||
<span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Name and Emoji in same row */}
|
||||
<div className="flex gap-4 items-start">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
{t('knowledge.kbName')}
|
||||
<span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="emoji"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('common.icon')}</FormLabel>
|
||||
<FormControl>
|
||||
<EmojiPicker
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
|
||||
@@ -70,6 +70,7 @@ export default function KnowledgePage() {
|
||||
id: kb.uuid || '',
|
||||
name: kb.name,
|
||||
description: kb.description,
|
||||
emoji: kb.emoji,
|
||||
embeddingModelUUID: kb.embedding_model_uuid,
|
||||
top_k: kb.top_k ?? 5,
|
||||
lastUpdatedTimeAgo: lastUpdatedTimeAgoText,
|
||||
@@ -102,6 +103,7 @@ export default function KnowledgePage() {
|
||||
id: kb.uuid || '',
|
||||
name: kb.name,
|
||||
description: kb.description,
|
||||
emoji: kb.emoji,
|
||||
retrieverName: `${kb.plugin_author}/${kb.plugin_name}/${kb.retriever_name}`,
|
||||
retrieverConfig: kb.retriever_config || {},
|
||||
lastUpdatedTimeAgo: lastUpdatedTimeAgoText,
|
||||
|
||||
@@ -8,12 +8,15 @@ export default function PipelineCard({ cardVO }: { cardVO: PipelineCardVO }) {
|
||||
return (
|
||||
<div className={`${styles.cardContainer}`}>
|
||||
<div className={`${styles.basicInfoContainer}`}>
|
||||
<div className={`${styles.basicInfoNameContainer}`}>
|
||||
<div className={`${styles.basicInfoNameText} ${styles.bigText}`}>
|
||||
{cardVO.name}
|
||||
</div>
|
||||
<div className={`${styles.basicInfoDescriptionText}`}>
|
||||
{cardVO.description}
|
||||
<div className={`${styles.iconBasicInfoContainer}`}>
|
||||
<div className={`${styles.iconEmoji}`}>{cardVO.emoji || '⚙️'}</div>
|
||||
<div className={`${styles.basicInfoNameContainer}`}>
|
||||
<div className={`${styles.basicInfoNameText} ${styles.bigText}`}>
|
||||
{cardVO.name}
|
||||
</div>
|
||||
<div className={`${styles.basicInfoDescriptionText}`}>
|
||||
{cardVO.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,8 +36,8 @@ export default function PipelineCard({ cardVO }: { cardVO: PipelineCardVO }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.operationContainer}>
|
||||
{cardVO.isDefault && (
|
||||
{cardVO.isDefault && (
|
||||
<div className={styles.operationContainer}>
|
||||
<div className={styles.operationDefaultBadge}>
|
||||
<svg
|
||||
className={styles.operationDefaultBadgeIcon}
|
||||
@@ -48,8 +51,8 @@ export default function PipelineCard({ cardVO }: { cardVO: PipelineCardVO }) {
|
||||
{t('pipelines.defaultBadge')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface IPipelineCardVO {
|
||||
description: string;
|
||||
lastUpdatedTimeAgo: string;
|
||||
isDefault: boolean;
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
export class PipelineCardVO implements IPipelineCardVO {
|
||||
@@ -12,6 +13,7 @@ export class PipelineCardVO implements IPipelineCardVO {
|
||||
name: string;
|
||||
lastUpdatedTimeAgo: string;
|
||||
isDefault: boolean;
|
||||
emoji?: string;
|
||||
|
||||
constructor(props: IPipelineCardVO) {
|
||||
this.id = props.id;
|
||||
@@ -19,5 +21,6 @@ export class PipelineCardVO implements IPipelineCardVO {
|
||||
this.description = props.description;
|
||||
this.lastUpdatedTimeAgo = props.lastUpdatedTimeAgo;
|
||||
this.isDefault = props.isDefault;
|
||||
this.emoji = props.emoji;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 2px 2px 0 rgba(0, 0, 0, 0.2);
|
||||
padding: 1.2rem;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -32,14 +32,41 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 0.4rem;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.iconEmoji {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:global(.dark) .iconEmoji {
|
||||
background-color: #2a2a2d;
|
||||
}
|
||||
|
||||
.iconBasicInfoContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.basicInfoNameContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.basicInfoNameText {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import EmojiPicker from '@/components/ui/emoji-picker';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -62,6 +63,7 @@ export default function PipelineFormComponent({
|
||||
description: z
|
||||
.string()
|
||||
.min(1, { message: t('pipelines.descriptionRequired') }),
|
||||
emoji: z.string().optional(),
|
||||
}),
|
||||
ai: z.record(z.string(), z.any()),
|
||||
trigger: z.record(z.string(), z.any()),
|
||||
@@ -74,6 +76,7 @@ export default function PipelineFormComponent({
|
||||
description: z
|
||||
.string()
|
||||
.min(1, { message: t('pipelines.descriptionRequired') }),
|
||||
emoji: z.string().optional(),
|
||||
}),
|
||||
ai: z.record(z.string(), z.any()).optional(),
|
||||
trigger: z.record(z.string(), z.any()).optional(),
|
||||
@@ -105,7 +108,9 @@ export default function PipelineFormComponent({
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
basic: {},
|
||||
basic: {
|
||||
emoji: '⚙️',
|
||||
},
|
||||
ai: {},
|
||||
trigger: {},
|
||||
safety: {},
|
||||
@@ -138,6 +143,7 @@ export default function PipelineFormComponent({
|
||||
basic: {
|
||||
name: resp.pipeline.name,
|
||||
description: resp.pipeline.description,
|
||||
emoji: resp.pipeline.emoji || '⚙️',
|
||||
},
|
||||
ai: resp.pipeline.config.ai,
|
||||
trigger: resp.pipeline.config.trigger,
|
||||
@@ -154,6 +160,7 @@ export default function PipelineFormComponent({
|
||||
basic: {
|
||||
name: '',
|
||||
description: '',
|
||||
emoji: '⚙️',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -172,6 +179,7 @@ export default function PipelineFormComponent({
|
||||
config: {},
|
||||
description: values.basic.description,
|
||||
name: values.basic.name,
|
||||
emoji: values.basic.emoji,
|
||||
};
|
||||
httpClient
|
||||
.createPipeline(pipeline)
|
||||
@@ -199,6 +207,7 @@ export default function PipelineFormComponent({
|
||||
description: values.basic.description,
|
||||
// for_version: '',
|
||||
name: values.basic.name,
|
||||
emoji: values.basic.emoji,
|
||||
// stages: [],
|
||||
// updated_at: '',
|
||||
// uuid: pipelineId || '',
|
||||
@@ -399,22 +408,41 @@ export default function PipelineFormComponent({
|
||||
>
|
||||
{formLabel.name === 'basic' && (
|
||||
<div className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="basic.name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('common.name')}
|
||||
<span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Name and Emoji in same row */}
|
||||
<div className="flex gap-4 items-start">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="basic.name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
{t('common.name')}
|
||||
<span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="basic.emoji"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('common.icon')}</FormLabel>
|
||||
<FormControl>
|
||||
<EmojiPicker
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -69,6 +69,7 @@ export default function PluginConfigPage() {
|
||||
description: pipeline.description,
|
||||
id: pipeline.uuid ?? '',
|
||||
name: pipeline.name,
|
||||
emoji: pipeline.emoji,
|
||||
isDefault: pipeline.is_default ?? false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,6 +78,7 @@ export interface KnowledgeBase {
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
top_k: number;
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
export interface ApiRespProviderEmbeddingModels {
|
||||
@@ -110,6 +111,7 @@ export interface Pipeline {
|
||||
is_default?: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
export interface ApiRespPlatformAdapters {
|
||||
@@ -168,6 +170,7 @@ export interface KnowledgeBase {
|
||||
top_k: number;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
export interface ExternalKnowledgeBase {
|
||||
@@ -179,6 +182,7 @@ export interface ExternalKnowledgeBase {
|
||||
plugin_name: string;
|
||||
retriever_name: string;
|
||||
retriever_config?: Record<string, unknown>;
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
export interface ApiRespExternalKnowledgeBases {
|
||||
@@ -306,6 +310,7 @@ interface GetPipeline {
|
||||
stages: string[];
|
||||
updated_at: string;
|
||||
uuid: string;
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
export interface GetPipelineResponseData {
|
||||
|
||||
253
web/src/components/ui/emoji-picker.tsx
Normal file
253
web/src/components/ui/emoji-picker.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
|
||||
interface EmojiPickerProps {
|
||||
value?: string;
|
||||
onChange: (emoji: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// 扩展的emoji分类
|
||||
const EMOJI_CATEGORIES = {
|
||||
common: [
|
||||
'⚙️',
|
||||
'📚',
|
||||
'🔗',
|
||||
'📁',
|
||||
'💡',
|
||||
'🎯',
|
||||
'✨',
|
||||
'🚀',
|
||||
'📝',
|
||||
'🔧',
|
||||
'⚡',
|
||||
'🔥',
|
||||
'💎',
|
||||
'🎨',
|
||||
'🎭',
|
||||
],
|
||||
objects: [
|
||||
'📦',
|
||||
'📂',
|
||||
'📋',
|
||||
'📌',
|
||||
'🔖',
|
||||
'💼',
|
||||
'🗂️',
|
||||
'📮',
|
||||
'🗃️',
|
||||
'📊',
|
||||
'📈',
|
||||
'📉',
|
||||
'🗄️',
|
||||
'📇',
|
||||
'🗳️',
|
||||
],
|
||||
symbols: [
|
||||
'🔴',
|
||||
'🟠',
|
||||
'🟡',
|
||||
'🟢',
|
||||
'🔵',
|
||||
'🟣',
|
||||
'⚪',
|
||||
'⚫',
|
||||
'🟤',
|
||||
'🔺',
|
||||
'🔻',
|
||||
'🔶',
|
||||
'🔷',
|
||||
'🔸',
|
||||
'🔹',
|
||||
],
|
||||
nature: [
|
||||
'🌟',
|
||||
'⭐',
|
||||
'🌈',
|
||||
'💧',
|
||||
'🌍',
|
||||
'🌙',
|
||||
'☀️',
|
||||
'🌱',
|
||||
'🌲',
|
||||
'🌳',
|
||||
'🌴',
|
||||
'🌵',
|
||||
'🌾',
|
||||
'🍀',
|
||||
'🌻',
|
||||
],
|
||||
faces: [
|
||||
'😀',
|
||||
'😊',
|
||||
'🤔',
|
||||
'😎',
|
||||
'🤖',
|
||||
'👾',
|
||||
'💬',
|
||||
'💭',
|
||||
'❤️',
|
||||
'⚠️',
|
||||
'✅',
|
||||
'❌',
|
||||
'🎉',
|
||||
'🎊',
|
||||
'🎈',
|
||||
],
|
||||
tech: [
|
||||
'💻',
|
||||
'📱',
|
||||
'⌨️',
|
||||
'🖥️',
|
||||
'🖱️',
|
||||
'💾',
|
||||
'💿',
|
||||
'📀',
|
||||
'🔌',
|
||||
'🔋',
|
||||
'📡',
|
||||
'🛰️',
|
||||
'🖨️',
|
||||
'🖲️',
|
||||
'💽',
|
||||
],
|
||||
science: [
|
||||
'🔬',
|
||||
'🔭',
|
||||
'⚗️',
|
||||
'🧪',
|
||||
'🧬',
|
||||
'🧫',
|
||||
'🩺',
|
||||
'💊',
|
||||
'💉',
|
||||
'🌡️',
|
||||
'🧲',
|
||||
'⚛️',
|
||||
'🧬',
|
||||
'🦠',
|
||||
'🧫',
|
||||
],
|
||||
business: [
|
||||
'💼',
|
||||
'📊',
|
||||
'📈',
|
||||
'💰',
|
||||
'💵',
|
||||
'💴',
|
||||
'💶',
|
||||
'💷',
|
||||
'💳',
|
||||
'💸',
|
||||
'📉',
|
||||
'💹',
|
||||
'🏦',
|
||||
'🏢',
|
||||
'🏭',
|
||||
],
|
||||
};
|
||||
|
||||
const CATEGORY_LABELS: { [key: string]: string } = {
|
||||
common: '常用',
|
||||
objects: '物品',
|
||||
symbols: '符号',
|
||||
nature: '自然',
|
||||
faces: '表情',
|
||||
tech: '科技',
|
||||
science: '科学',
|
||||
business: '商业',
|
||||
};
|
||||
|
||||
// 每个分类的代表性 emoji(用于分页按钮)
|
||||
const CATEGORY_ICONS: { [key: string]: string } = {
|
||||
common: '⭐',
|
||||
objects: '📦',
|
||||
symbols: '🔴',
|
||||
nature: '🌟',
|
||||
faces: '😀',
|
||||
tech: '💻',
|
||||
science: '🔬',
|
||||
business: '💼',
|
||||
};
|
||||
|
||||
export default function EmojiPicker({
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
}: EmojiPickerProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [activeCategory, setActiveCategory] = useState<string>('common');
|
||||
|
||||
const handleEmojiSelect = (emoji: string) => {
|
||||
onChange(emoji);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const currentEmojis =
|
||||
EMOJI_CATEGORIES[activeCategory as keyof typeof EMOJI_CATEGORIES];
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={disabled}
|
||||
className="w-16 h-16 text-3xl p-0 hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
type="button"
|
||||
>
|
||||
{value || '😀'}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 p-4" align="start">
|
||||
<div className="space-y-3">
|
||||
{/* 分类标题 */}
|
||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{CATEGORY_LABELS[activeCategory]}
|
||||
</h3>
|
||||
|
||||
{/* Emoji 网格 */}
|
||||
<div className="grid grid-cols-6 gap-1">
|
||||
{currentEmojis.map((emoji, index) => (
|
||||
<button
|
||||
key={`${activeCategory}-${index}`}
|
||||
type="button"
|
||||
onClick={() => handleEmojiSelect(emoji)}
|
||||
className={`w-10 h-10 text-xl rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors flex items-center justify-center ${
|
||||
value === emoji ? 'bg-gray-200 dark:bg-gray-700' : ''
|
||||
}`}
|
||||
>
|
||||
{emoji}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 分类切换按钮 */}
|
||||
<div className="pt-2 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-center gap-1">
|
||||
{Object.keys(EMOJI_CATEGORIES).map((category) => (
|
||||
<button
|
||||
key={category}
|
||||
type="button"
|
||||
onClick={() => setActiveCategory(category)}
|
||||
className={`w-7 h-7 text-base rounded transition-colors flex items-center justify-center ${
|
||||
activeCategory === category
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
title={CATEGORY_LABELS[category]}
|
||||
>
|
||||
{CATEGORY_ICONS[category]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -37,6 +37,7 @@ const enUS = {
|
||||
enable: 'Enable',
|
||||
name: 'Name',
|
||||
description: 'Description',
|
||||
icon: 'Icon',
|
||||
close: 'Close',
|
||||
deleteSuccess: 'Deleted successfully',
|
||||
deleteError: 'Delete failed: ',
|
||||
|
||||
@@ -38,6 +38,7 @@ const jaJP = {
|
||||
enable: '有効にする',
|
||||
name: '名前',
|
||||
description: '説明',
|
||||
icon: 'アイコン',
|
||||
close: '閉じる',
|
||||
deleteSuccess: '削除に成功しました',
|
||||
deleteError: '削除に失敗しました:',
|
||||
|
||||
@@ -37,6 +37,7 @@ const zhHans = {
|
||||
enable: '是否启用',
|
||||
name: '名称',
|
||||
description: '描述',
|
||||
icon: '图标',
|
||||
close: '关闭',
|
||||
deleteSuccess: '删除成功',
|
||||
deleteError: '删除失败:',
|
||||
|
||||
@@ -37,6 +37,7 @@ const zhHant = {
|
||||
enable: '是否啟用',
|
||||
name: '名稱',
|
||||
description: '描述',
|
||||
icon: '圖標',
|
||||
close: '關閉',
|
||||
deleteSuccess: '刪除成功',
|
||||
deleteError: '刪除失敗:',
|
||||
|
||||
Reference in New Issue
Block a user