feature:新增提示词工坊功能

This commit is contained in:
xiamuceer-j
2026-01-27 13:57:32 +08:00
parent 0c3fc6c912
commit 7b72691080
15 changed files with 2252 additions and 27 deletions
+721
View File
@@ -0,0 +1,721 @@
import { useState, useEffect, useCallback } from 'react';
import {
Card,
Row,
Col,
Input,
Select,
Button,
Tag,
Space,
Empty,
Spin,
Modal,
Form,
message,
Tooltip,
Badge,
Tabs,
Typography,
Pagination,
Alert,
} from 'antd';
import {
SearchOutlined,
DownloadOutlined,
HeartOutlined,
HeartFilled,
CloudUploadOutlined,
EyeOutlined,
UserOutlined,
ClockCircleOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
SyncOutlined,
DeleteOutlined,
CloudOutlined,
DisconnectOutlined,
} from '@ant-design/icons';
import { promptWorkshopApi } from '../services/api';
import type {
PromptWorkshopItem,
PromptSubmission,
PromptSubmissionCreate,
} from '../types';
import { PROMPT_CATEGORIES } from '../types';
const { TextArea } = Input;
const { Text, Paragraph } = Typography;
interface PromptWorkshopProps {
onImportSuccess?: () => void;
}
export default function PromptWorkshop({ onImportSuccess }: PromptWorkshopProps) {
const [items, setItems] = useState<PromptWorkshopItem[]>([]);
const [loading, setLoading] = useState(false);
const [total, setTotal] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize] = useState(12);
// 筛选条件
const [category, setCategory] = useState<string>('');
const [searchKeyword, setSearchKeyword] = useState('');
const [sortBy, setSortBy] = useState<'newest' | 'popular' | 'downloads'>('newest');
// 服务状态
const [serviceStatus, setServiceStatus] = useState<{
mode: string;
instance_id: string;
cloud_connected?: boolean;
} | null>(null);
// 提交相关
const [isSubmitModalOpen, setIsSubmitModalOpen] = useState(false);
const [submitLoading, setSubmitLoading] = useState(false);
const [submitForm] = Form.useForm();
// 我的提交
const [mySubmissions, setMySubmissions] = useState<PromptSubmission[]>([]);
const [submissionsLoading, setSubmissionsLoading] = useState(false);
// 详情弹窗
const [detailItem, setDetailItem] = useState<PromptWorkshopItem | null>(null);
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
// 导入状态
const [importingId, setImportingId] = useState<string | null>(null);
const isMobile = window.innerWidth <= 768;
// 加载服务状态
useEffect(() => {
const checkStatus = async () => {
try {
const status = await promptWorkshopApi.getStatus();
setServiceStatus(status);
} catch (error) {
console.error('Failed to check workshop status:', error);
}
};
checkStatus();
}, []);
// 加载工坊列表
const loadItems = useCallback(async () => {
setLoading(true);
try {
const response = await promptWorkshopApi.getItems({
category: category || undefined,
search: searchKeyword || undefined,
sort: sortBy,
page: currentPage,
limit: pageSize,
});
setItems(response.data?.items || []);
setTotal(response.data?.total || 0);
} catch (error) {
console.error('Failed to load workshop items:', error);
message.error('加载提示词工坊失败');
} finally {
setLoading(false);
}
}, [category, searchKeyword, sortBy, currentPage, pageSize]);
useEffect(() => {
loadItems();
}, [loadItems]);
// 加载我的提交
const loadMySubmissions = async () => {
setSubmissionsLoading(true);
try {
const response = await promptWorkshopApi.getMySubmissions();
setMySubmissions(response.data?.items || []);
} catch (error) {
console.error('Failed to load submissions:', error);
} finally {
setSubmissionsLoading(false);
}
};
// 导入到本地
const handleImport = async (item: PromptWorkshopItem) => {
setImportingId(item.id);
try {
await promptWorkshopApi.importItem(item.id);
message.success(`已导入「${item.name}」到本地写作风格`);
onImportSuccess?.();
// 刷新列表更新下载计数
loadItems();
} catch (error) {
console.error('Failed to import item:', error);
message.error('导入失败');
} finally {
setImportingId(null);
}
};
// 点赞
const handleLike = async (item: PromptWorkshopItem) => {
try {
const response = await promptWorkshopApi.toggleLike(item.id);
// 更新本地状态
setItems(prev => prev.map(i =>
i.id === item.id
? { ...i, is_liked: response.liked, like_count: response.like_count }
: i
));
} catch (error) {
console.error('Failed to toggle like:', error);
message.error('操作失败');
}
};
// 提交新提示词
const handleSubmit = async (values: PromptSubmissionCreate) => {
setSubmitLoading(true);
try {
await promptWorkshopApi.submit({
...values,
tags: values.tags ? (values.tags as unknown as string).split(',').map((t: string) => t.trim()).filter(Boolean) : [],
});
message.success('提交成功,等待管理员审核');
setIsSubmitModalOpen(false);
submitForm.resetFields();
loadMySubmissions();
} catch (error) {
console.error('Failed to submit:', error);
message.error('提交失败');
} finally {
setSubmitLoading(false);
}
};
// 撤回提交
const handleWithdraw = async (submissionId: string) => {
try {
await promptWorkshopApi.withdrawSubmission(submissionId);
message.success('已撤回');
loadMySubmissions();
} catch (error) {
console.error('Failed to withdraw:', error);
message.error('撤回失败');
}
};
// 查看详情
const handleViewDetail = async (item: PromptWorkshopItem) => {
try {
const response = await promptWorkshopApi.getItem(item.id);
setDetailItem(response.data);
setIsDetailModalOpen(true);
} catch (error) {
console.error('Failed to load detail:', error);
message.error('加载详情失败');
}
};
// 获取分类标签颜色
const getCategoryColor = (cat: string) => {
const colors: Record<string, string> = {
general: 'blue',
fantasy: 'purple',
martial: 'orange',
romance: 'pink',
scifi: 'cyan',
horror: 'red',
history: 'gold',
urban: 'green',
game: 'magenta',
other: 'default',
};
return colors[cat] || 'default';
};
// 获取分类名称
const getCategoryName = (cat: string) => {
return PROMPT_CATEGORIES[cat] || cat;
};
// 获取分类选项列表
const categoryOptions = Object.entries(PROMPT_CATEGORIES).map(([value, label]) => ({
value,
label,
}));
// 获取提交状态标签
const getStatusTag = (status: string) => {
const config: Record<string, { color: string; icon: React.ReactNode; text: string }> = {
pending: { color: 'processing', icon: <ClockCircleOutlined />, text: '待审核' },
approved: { color: 'success', icon: <CheckCircleOutlined />, text: '已通过' },
rejected: { color: 'error', icon: <CloseCircleOutlined />, text: '已拒绝' },
};
const cfg = config[status] || config.pending;
return <Tag color={cfg.color} icon={cfg.icon}>{cfg.text}</Tag>;
};
// 网格配置
const gridConfig = {
gutter: isMobile ? 8 : 16,
xs: 24,
sm: 12,
md: 8,
lg: 6,
xl: 6,
};
// 渲染工坊列表
const renderWorkshopList = () => (
<div>
{/* 服务状态 */}
{serviceStatus && !serviceStatus.cloud_connected && serviceStatus.mode === 'client' && (
<Alert
type="warning"
message="云端服务未连接"
description="无法访问提示词工坊,请检查网络连接或稍后重试"
icon={<DisconnectOutlined />}
showIcon
style={{ marginBottom: 16 }}
/>
)}
{/* 筛选区域 */}
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: 12,
marginBottom: 16,
alignItems: 'center',
}}>
<Input
placeholder="搜索提示词..."
prefix={<SearchOutlined />}
value={searchKeyword}
onChange={e => setSearchKeyword(e.target.value)}
onPressEnter={() => { setCurrentPage(1); loadItems(); }}
style={{ width: isMobile ? '100%' : 200 }}
allowClear
/>
<Select
placeholder="选择分类"
value={category}
onChange={v => { setCategory(v); setCurrentPage(1); }}
style={{ width: isMobile ? '100%' : 150 }}
allowClear
>
{categoryOptions.map(cat => (
<Select.Option key={cat.value} value={cat.value}>{cat.label}</Select.Option>
))}
</Select>
<Select
value={sortBy}
onChange={v => { setSortBy(v); setCurrentPage(1); }}
style={{ width: isMobile ? '100%' : 120 }}
>
<Select.Option value="newest"></Select.Option>
<Select.Option value="popular"></Select.Option>
<Select.Option value="downloads"></Select.Option>
</Select>
<Button
icon={<SyncOutlined />}
onClick={() => { setCurrentPage(1); loadItems(); }}
>
</Button>
</div>
{/* 列表区域 */}
<Spin spinning={loading}>
{items.length === 0 ? (
<Empty description="暂无提示词" />
) : (
<>
<Row gutter={[gridConfig.gutter, gridConfig.gutter]}>
{items.map(item => (
<Col
key={item.id}
xs={gridConfig.xs}
sm={gridConfig.sm}
md={gridConfig.md}
lg={gridConfig.lg}
xl={gridConfig.xl}
>
<Card
hoverable
style={{ height: '100%', borderRadius: 12 }}
bodyStyle={{ padding: 16, display: 'flex', flexDirection: 'column', height: '100%' }}
actions={[
<Tooltip title="查看详情" key="view">
<EyeOutlined onClick={() => handleViewDetail(item)} />
</Tooltip>,
<Tooltip title={item.is_liked ? '取消点赞' : '点赞'} key="like">
<span onClick={() => handleLike(item)}>
{item.is_liked ? (
<HeartFilled style={{ color: '#ff4d4f' }} />
) : (
<HeartOutlined />
)}
<span style={{ marginLeft: 4 }}>{item.like_count || 0}</span>
</span>
</Tooltip>,
<Tooltip title="导入到本地" key="import">
<Button
type="link"
size="small"
icon={<DownloadOutlined />}
loading={importingId === item.id}
onClick={() => handleImport(item)}
>
{item.download_count || 0}
</Button>
</Tooltip>,
]}
>
<div style={{ flex: 1 }}>
<Space style={{ marginBottom: 8 }} wrap>
<Text strong style={{ fontSize: 15 }}>{item.name}</Text>
<Tag color={getCategoryColor(item.category)}>
{getCategoryName(item.category)}
</Tag>
</Space>
{item.description && (
<Paragraph
type="secondary"
style={{ fontSize: 13, marginBottom: 8 }}
ellipsis={{ rows: 2 }}
>
{item.description}
</Paragraph>
)}
<Paragraph
style={{
fontSize: 12,
backgroundColor: '#fafafa',
padding: 8,
borderRadius: 4,
marginBottom: 8,
}}
ellipsis={{ rows: 3 }}
>
{item.prompt_content}
</Paragraph>
{item.tags && item.tags.length > 0 && (
<Space size={4} wrap>
{item.tags.slice(0, 3).map(tag => (
<Tag key={tag} style={{ fontSize: 11 }}>{tag}</Tag>
))}
{item.tags.length > 3 && (
<Tag style={{ fontSize: 11 }}>+{item.tags.length - 3}</Tag>
)}
</Space>
)}
</div>
<div style={{ marginTop: 8, color: '#999', fontSize: 12 }}>
<Space>
<span><UserOutlined /> {item.author_name || '匿名'}</span>
</Space>
</div>
</Card>
</Col>
))}
</Row>
{total > pageSize && (
<div style={{ marginTop: 24, textAlign: 'center' }}>
<Pagination
current={currentPage}
total={total}
pageSize={pageSize}
onChange={page => setCurrentPage(page)}
showSizeChanger={false}
showTotal={t => `${t} 个提示词`}
/>
</div>
)}
</>
)}
</Spin>
</div>
);
// 渲染我的提交
const renderMySubmissions = () => (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Text></Text>
<Button icon={<SyncOutlined />} onClick={loadMySubmissions}>
</Button>
</div>
<Spin spinning={submissionsLoading}>
{mySubmissions.length === 0 ? (
<Empty description="暂无提交记录" />
) : (
<Row gutter={[16, 16]}>
{mySubmissions.map(sub => (
<Col key={sub.id} xs={24} sm={12} md={8} lg={6}>
<Card
style={{ borderRadius: 12 }}
bodyStyle={{ padding: 16 }}
>
<Space direction="vertical" style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Text strong>{sub.name}</Text>
{getStatusTag(sub.status)}
</div>
<Tag color={getCategoryColor(sub.category)}>
{getCategoryName(sub.category)}
</Tag>
<Paragraph
type="secondary"
style={{ fontSize: 12, marginBottom: 0 }}
ellipsis={{ rows: 2 }}
>
{sub.prompt_content}
</Paragraph>
{sub.status === 'rejected' && sub.review_note && (
<Alert
type="error"
message="拒绝原因"
description={sub.review_note}
style={{ fontSize: 12 }}
/>
)}
<div style={{ fontSize: 12, color: '#999' }}>
: {sub.created_at ? new Date(sub.created_at).toLocaleDateString() : '-'}
</div>
{sub.status === 'pending' && (
<Button
type="link"
danger
size="small"
icon={<DeleteOutlined />}
onClick={() => handleWithdraw(sub.id)}
>
</Button>
)}
</Space>
</Card>
</Col>
))}
</Row>
)}
</Spin>
</div>
);
return (
<div>
{/* 标题和操作区 */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
flexWrap: 'wrap',
gap: 12,
}}>
<Space>
<CloudOutlined style={{ fontSize: 20 }} />
<Text strong style={{ fontSize: 16 }}></Text>
{serviceStatus?.mode === 'server' && (
<Badge status="success" text="服务端模式" />
)}
</Space>
<Button
type="primary"
icon={<CloudUploadOutlined />}
onClick={() => setIsSubmitModalOpen(true)}
>
</Button>
</div>
{/* 标签页 */}
<Tabs
defaultActiveKey="browse"
onChange={key => key === 'submissions' && loadMySubmissions()}
items={[
{
key: 'browse',
label: '浏览工坊',
children: renderWorkshopList(),
},
{
key: 'submissions',
label: (
<Badge count={mySubmissions.filter(s => s.status === 'pending').length} size="small">
</Badge>
),
children: renderMySubmissions(),
},
]}
/>
{/* 提交弹窗 */}
<Modal
title="分享提示词到工坊"
open={isSubmitModalOpen}
onCancel={() => {
setIsSubmitModalOpen(false);
submitForm.resetFields();
}}
footer={null}
width={isMobile ? '100%' : 600}
>
<Alert
type="info"
message="提交须知"
description="您的提示词将提交给管理员审核,审核通过后会在工坊中展示。请确保内容原创且不含敏感信息。"
style={{ marginBottom: 16 }}
showIcon
/>
<Form
form={submitForm}
layout="vertical"
onFinish={handleSubmit}
>
<Form.Item
name="name"
label="名称"
rules={[{ required: true, message: '请输入名称' }]}
>
<Input placeholder="给您的提示词起个名字" maxLength={50} />
</Form.Item>
<Form.Item
name="category"
label="分类"
rules={[{ required: true, message: '请选择分类' }]}
>
<Select placeholder="选择分类">
{categoryOptions.map(cat => (
<Select.Option key={cat.value} value={cat.value}>{cat.label}</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item name="description" label="描述">
<TextArea rows={2} placeholder="简要描述这个提示词的用途和效果" maxLength={200} />
</Form.Item>
<Form.Item
name="prompt_content"
label="提示词内容"
rules={[{ required: true, message: '请输入提示词内容' }]}
>
<TextArea rows={6} placeholder="输入完整的提示词内容..." />
</Form.Item>
<Form.Item name="tags" label="标签">
<Input placeholder="输入标签,多个用逗号分隔,如: 武侠,对话,细腻" />
</Form.Item>
<Form.Item>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => {
setIsSubmitModalOpen(false);
submitForm.resetFields();
}}>
</Button>
<Button type="primary" htmlType="submit" loading={submitLoading}>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
{/* 详情弹窗 */}
<Modal
title={detailItem?.name}
open={isDetailModalOpen}
onCancel={() => {
setIsDetailModalOpen(false);
setDetailItem(null);
}}
footer={[
<Button key="close" onClick={() => setIsDetailModalOpen(false)}>
</Button>,
<Button
key="import"
type="primary"
icon={<DownloadOutlined />}
loading={importingId === detailItem?.id}
onClick={() => detailItem && handleImport(detailItem)}
>
</Button>,
]}
width={isMobile ? '100%' : 700}
>
{detailItem && (
<div>
<Space style={{ marginBottom: 16 }} wrap>
<Tag color={getCategoryColor(detailItem.category)}>
{getCategoryName(detailItem.category)}
</Tag>
{detailItem.tags?.map(tag => (
<Tag key={tag}>{tag}</Tag>
))}
</Space>
{detailItem.description && (
<Paragraph style={{ marginBottom: 16 }}>
{detailItem.description}
</Paragraph>
)}
<div style={{
backgroundColor: '#f5f5f5',
padding: 16,
borderRadius: 8,
marginBottom: 16,
}}>
<Text strong style={{ display: 'block', marginBottom: 8 }}></Text>
<pre style={{
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
margin: 0,
fontSize: 13,
}}>
{detailItem.prompt_content}
</pre>
</div>
<Row gutter={16}>
<Col span={8}>
<Text type="secondary"></Text>
<div><UserOutlined /> {detailItem.author_name || '匿名'}</div>
</Col>
<Col span={8}>
<Text type="secondary"></Text>
<div><HeartOutlined /> {detailItem.like_count || 0}</div>
</Col>
<Col span={8}>
<Text type="secondary"></Text>
<div><DownloadOutlined /> {detailItem.download_count || 0}</div>
</Col>
</Row>
</div>
)}
</Modal>
</div>
);
}
+63 -24
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import {
Button,
Modal,
@@ -12,18 +12,22 @@ import {
Empty,
Typography,
Row,
Col
Col,
Tabs,
Badge,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
StarOutlined,
StarFilled
StarFilled,
CloudOutlined,
} from '@ant-design/icons';
import { useStore } from '../store';
import { writingStyleApi } from '../services/api';
import type { WritingStyle, WritingStyleCreate, WritingStyleUpdate } from '../types';
import PromptWorkshop from '../components/PromptWorkshop';
const { TextArea } = Input;
const { Text, Paragraph } = Typography;
@@ -56,7 +60,7 @@ export default function WritingStyles() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentProject?.id]);
const loadStyles = async () => {
const loadStyles = useCallback(async () => {
try {
setLoading(true);
// 如果有当前项目,使用项目API获取(包含is_default标记)
@@ -80,7 +84,7 @@ export default function WritingStyles() {
} finally {
setLoading(false);
}
};
}, [currentProject?.id]);
const handleCreate = async (values: { name: string; description?: string; prompt_content: string }) => {
try {
@@ -164,36 +168,23 @@ export default function WritingStyles() {
return styleType === 'preset' ? '预设' : '自定义';
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
// 渲染本地风格列表
const renderLocalStyles = () => (
<div>
<div style={{
position: 'sticky',
top: 0,
zIndex: 10,
backgroundColor: '#fff',
padding: isMobile ? '12px 0' : '16px 0',
marginBottom: isMobile ? 12 : 16,
borderBottom: '1px solid #f0f0f0',
marginBottom: 16,
display: 'flex',
flexDirection: isMobile ? 'column' : 'row',
gap: isMobile ? 12 : 0,
justifyContent: 'space-between',
alignItems: isMobile ? 'stretch' : 'center'
justifyContent: 'flex-end',
}}>
<h2 style={{ margin: 0, fontSize: isMobile ? 18 : 24 }}>
<EditOutlined style={{ marginRight: 8 }} />
</h2>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={showCreateModal}
block={isMobile}
>
</Button>
</div>
<div style={{ flex: 1, overflowY: 'auto' }}>
{styles.length === 0 ? (
<Empty description="暂无风格数据" />
@@ -312,6 +303,54 @@ export default function WritingStyles() {
</Row>
)}
</div>
</div>
);
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{
position: 'sticky',
top: 0,
zIndex: 10,
backgroundColor: '#fff',
padding: isMobile ? '12px 0' : '16px 0',
marginBottom: isMobile ? 12 : 16,
borderBottom: '1px solid #f0f0f0',
}}>
<h2 style={{ margin: 0, fontSize: isMobile ? 18 : 24 }}>
<EditOutlined style={{ marginRight: 8 }} />
</h2>
</div>
<Tabs
defaultActiveKey="local"
style={{ flex: 1 }}
items={[
{
key: 'local',
label: (
<span>
<EditOutlined />
</span>
),
children: renderLocalStyles(),
},
{
key: 'workshop',
label: (
<Badge dot>
<span>
<CloudOutlined />
</span>
</Badge>
),
children: <PromptWorkshop onImportSuccess={loadStyles} />,
},
]}
/>
{/* 创建自定义风格 Modal */}
<Modal
+52
View File
@@ -34,6 +34,10 @@ import type {
WritingStyleUpdate,
PresetStyle,
WritingStyleListResponse,
PromptWorkshopListResponse,
PromptWorkshopItem,
PromptSubmission,
PromptSubmissionCreate,
MCPPlugin,
MCPPluginCreate,
MCPPluginUpdate,
@@ -648,6 +652,54 @@ export const writingStyleApi = {
api.post<unknown, WritingStyleListResponse>(`/writing-styles/project/${projectId}/initialize`, {}),
};
export const promptWorkshopApi = {
// 检查服务状态
getStatus: () =>
api.get<unknown, { mode: string; instance_id: string; cloud_url?: string; cloud_connected?: boolean }>('/prompt-workshop/status'),
// 获取工坊提示词列表
getItems: (params?: {
category?: string;
search?: string;
tags?: string;
sort?: 'newest' | 'popular' | 'downloads';
page?: number;
limit?: number;
}) => api.get<unknown, PromptWorkshopListResponse>('/prompt-workshop/items', { params }),
// 获取单个提示词
getItem: (itemId: string) =>
api.get<unknown, { success: boolean; data: PromptWorkshopItem }>(`/prompt-workshop/items/${itemId}`),
// 导入到本地
importItem: (itemId: string, customName?: string) =>
api.post<unknown, { success: boolean; message: string; writing_style: WritingStyle }>(
`/prompt-workshop/items/${itemId}/import`,
{ custom_name: customName }
),
// 点赞
toggleLike: (itemId: string) =>
api.post<unknown, { success: boolean; liked: boolean; like_count: number }>(
`/prompt-workshop/items/${itemId}/like`
),
// 提交提示词
submit: (data: PromptSubmissionCreate) =>
api.post<unknown, { success: boolean; message: string; submission: PromptSubmission }>('/prompt-workshop/submit', data),
// 我的提交
getMySubmissions: (status?: string) =>
api.get<unknown, { success: boolean; data: { total: number; items: PromptSubmission[] } }>(
'/prompt-workshop/my-submissions',
{ params: { status } }
),
// 撤回提交
withdrawSubmission: (submissionId: string) =>
api.delete<unknown, { success: boolean; message: string }>(`/prompt-workshop/submissions/${submissionId}`),
};
export const polishApi = {
polishText: (data: PolishTextRequest) =>
api.post<unknown, { polished_text: string }>('/polish', data),
+92 -1
View File
@@ -880,4 +880,95 @@ export interface ForeshadowContextResponse {
pending_resolve: Foreshadow[];
overdue: Foreshadow[];
recently_planted: Foreshadow[];
}
}
// ==================== 提示词工坊类型定义 ====================
export interface PromptWorkshopItem {
id: string;
name: string;
description?: string;
prompt_content: string;
category: string;
tags?: string[];
author_name?: string;
is_official: boolean;
download_count: number;
like_count: number;
is_liked?: boolean;
created_at?: string;
}
export interface PromptSubmission {
id: string;
name: string;
description?: string;
prompt_content?: string;
category: string;
tags?: string[];
author_display_name?: string;
is_anonymous: boolean;
status: 'pending' | 'approved' | 'rejected';
review_note?: string;
reviewed_at?: string;
created_at?: string;
source_instance?: string;
submitter_name?: string;
}
export interface PromptSubmissionCreate {
name: string;
description?: string;
prompt_content: string;
category: string;
tags?: string[];
author_display_name?: string;
is_anonymous?: boolean;
source_style_id?: number;
}
export interface PromptWorkshopCategory {
id: string;
name: string;
count: number;
}
export interface PromptWorkshopListResponse {
success: boolean;
data: {
total: number;
page: number;
limit: number;
items: PromptWorkshopItem[];
categories: PromptWorkshopCategory[];
};
}
export interface PromptWorkshopStatusResponse {
mode: 'client' | 'server';
instance_id: string;
cloud_url?: string;
cloud_connected?: boolean;
}
export interface PromptWorkshopAdminStats {
total_items: number;
total_official: number;
total_pending: number;
total_downloads: number;
total_likes: number;
}
// 提示词工坊分类常量
export const PROMPT_CATEGORIES: Record<string, string> = {
general: '通用',
fantasy: '玄幻/仙侠',
martial: '武侠',
romance: '言情',
scifi: '科幻',
horror: '悬疑/惊悚',
history: '历史',
urban: '都市',
game: '游戏/电竞',
other: '其他',
};