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:
Junyan Qin (Chin)
2026-01-26 17:37:35 +08:00
committed by GitHub
parent d6e1e79f07
commit b73847f1a6
24 changed files with 3701 additions and 1173 deletions

View File

@@ -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,

View File

@@ -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)

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View File

@@ -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;

View File

@@ -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

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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"

View File

@@ -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,

View File

@@ -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>
);
}

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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}

View File

@@ -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,
});
});

View File

@@ -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 {

View 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>
);
}

View File

@@ -37,6 +37,7 @@ const enUS = {
enable: 'Enable',
name: 'Name',
description: 'Description',
icon: 'Icon',
close: 'Close',
deleteSuccess: 'Deleted successfully',
deleteError: 'Delete failed: ',

View File

@@ -38,6 +38,7 @@ const jaJP = {
enable: '有効にする',
name: '名前',
description: '説明',
icon: 'アイコン',
close: '閉じる',
deleteSuccess: '削除に成功しました',
deleteError: '削除に失敗しました:',

View File

@@ -37,6 +37,7 @@ const zhHans = {
enable: '是否启用',
name: '名称',
description: '描述',
icon: '图标',
close: '关闭',
deleteSuccess: '删除成功',
deleteError: '删除失败:',

View File

@@ -37,6 +37,7 @@ const zhHant = {
enable: '是否啟用',
name: '名稱',
description: '描述',
icon: '圖標',
close: '關閉',
deleteSuccess: '刪除成功',
deleteError: '刪除失敗:',