feat: Prompt Envelope Protocol MVP
- 定义 11 种 Segment 类型(text, static_var, system_prompt, memory, skills, tool_overview, tool_call_request/result, document, long_text, media) - 每种 Segment 有独立的颜色编码和折叠交互 - 通用 CollapsiblePanel + SegmentRenderer 路由架构 - 4 个 Demo 场景覆盖全部 9 种上下文类型 - 导出为 OpenAI Chat Completions Format(model + messages + tools) - tool_overview -> 请求级 tools[](含 JSON Schema) - tool_call_request -> assistant.tool_calls[] - tool_call_result -> tool-role message(ID 配对) - 17 个单元测试全部通过 - React 18 + TypeScript + Vite + Tailwind CSS
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import type { DocumentSegment } from '../../types/protocol'
|
||||
import { FileText, FileImage, FileAudio, File } from 'lucide-react'
|
||||
|
||||
const mimeIcons: Record<string, React.ComponentType<{ size?: number; className?: string }>> = {
|
||||
'text/': FileText,
|
||||
'image/': FileImage,
|
||||
'audio/': FileAudio,
|
||||
}
|
||||
|
||||
function getIcon(mimeType: string) {
|
||||
for (const [prefix, Icon] of Object.entries(mimeIcons)) {
|
||||
if (mimeType.startsWith(prefix)) return <Icon size={20} className="text-gray-500" />
|
||||
}
|
||||
return <File size={20} className="text-gray-500" />
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes}B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`
|
||||
}
|
||||
|
||||
export default function DocumentCard({ segment }: { segment: DocumentSegment }) {
|
||||
return (
|
||||
<div className="my-2 flex items-start gap-3 rounded-lg border border-gray-200 bg-white p-3 hover:shadow-sm transition-shadow">
|
||||
<div className="shrink-0 mt-0.5">{getIcon(segment.mimeType)}</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm font-medium text-gray-800 truncate">
|
||||
{segment.fileName}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">{formatBytes(segment.sizeBytes)}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-0.5">{segment.mimeType}</p>
|
||||
<p className="text-xs text-gray-500 mt-1.5 line-clamp-3 italic">
|
||||
{segment.snippet}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useState } from 'react'
|
||||
import type { LongTextSegment } from '../../types/protocol'
|
||||
import { ChevronDown, ChevronRight, Text } from 'lucide-react'
|
||||
|
||||
export default function LongTextView({ segment }: { segment: LongTextSegment }) {
|
||||
const [collapsed, setCollapsed] = useState(segment.collapsed)
|
||||
|
||||
// Show first ~2 lines when collapsed
|
||||
const preview = segment.content.split('\n').slice(0, 2).join('\n')
|
||||
|
||||
return (
|
||||
<div className="my-2 rounded-lg border border-gray-200 bg-white overflow-hidden">
|
||||
<button
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<Text size={14} className="text-gray-400" />
|
||||
<span className="text-xs text-gray-500 flex-1">
|
||||
{collapsed ? '长文本素材' : '长文本素材'} · {segment.charCount} 字
|
||||
</span>
|
||||
<span className="text-blue-500 text-xs">
|
||||
{collapsed ? '展开' : '收起'}
|
||||
</span>
|
||||
<span className="text-gray-400">
|
||||
{collapsed ? <ChevronRight size={14} /> : <ChevronDown size={14} />}
|
||||
</span>
|
||||
</button>
|
||||
{!collapsed && (
|
||||
<div className="px-3 pb-3">
|
||||
<p className="text-sm text-gray-700 whitespace-pre-wrap leading-relaxed max-h-64 overflow-y-auto">
|
||||
{segment.content}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{collapsed && (
|
||||
<div className="px-3 pb-2">
|
||||
<p className="text-sm text-gray-400 whitespace-pre-wrap line-clamp-2 italic">
|
||||
{preview}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { MediaSegment } from '../../types/protocol'
|
||||
import { Image, Music, Video } from 'lucide-react'
|
||||
|
||||
const mediaConfig = {
|
||||
image: { icon: Image, label: '图片', bg: 'bg-blue-50', color: 'text-blue-600' },
|
||||
audio: { icon: Music, label: '音频', bg: 'bg-pink-50', color: 'text-pink-600' },
|
||||
video: { icon: Video, label: '视频', bg: 'bg-indigo-50', color: 'text-indigo-600' },
|
||||
}
|
||||
|
||||
export default function MediaView({ segment }: { segment: MediaSegment }) {
|
||||
const cfg = mediaConfig[segment.mediaType]
|
||||
const Icon = cfg.icon
|
||||
|
||||
return (
|
||||
<div className={`my-2 flex items-center gap-3 rounded-lg border px-3 py-2.5 ${cfg.bg} border-gray-200`}>
|
||||
<Icon size={20} className={cfg.color} />
|
||||
<div className="flex-1">
|
||||
<span className={`text-sm font-medium ${cfg.color}`}>{cfg.label}</span>
|
||||
{segment.altText && (
|
||||
<span className="text-xs text-gray-500 ml-2">{segment.altText}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-400">{segment.mediaType}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { MemorySegment } from '../../types/protocol'
|
||||
import CollapsiblePanel from '../CollapsiblePanel'
|
||||
import { Brain } from 'lucide-react'
|
||||
|
||||
export default function MemoryView({ segment }: { segment: MemorySegment }) {
|
||||
return (
|
||||
<CollapsiblePanel
|
||||
title="User Memory"
|
||||
icon={<Brain size={16} />}
|
||||
color="border-purple-400 text-purple-700"
|
||||
bgColor="bg-purple-50"
|
||||
defaultCollapsed={segment.collapsed}
|
||||
badge={`${segment.items.length} 条记忆`}
|
||||
>
|
||||
<ul className="space-y-2">
|
||||
{segment.items.map((item, i) => (
|
||||
<li key={i} className="bg-white/60 rounded px-2 py-1.5">
|
||||
<div className="text-xs font-semibold text-purple-800">{item.title}</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">{item.content}</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CollapsiblePanel>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { SkillSegment } from '../../types/protocol'
|
||||
import CollapsiblePanel from '../CollapsiblePanel'
|
||||
import { Zap } from 'lucide-react'
|
||||
|
||||
export default function SkillsView({ segment }: { segment: SkillSegment }) {
|
||||
return (
|
||||
<CollapsiblePanel
|
||||
title="Skills"
|
||||
icon={<Zap size={16} />}
|
||||
color="border-green-400 text-green-700"
|
||||
bgColor="bg-green-50"
|
||||
defaultCollapsed={segment.collapsed}
|
||||
badge={`${segment.items.length} skills`}
|
||||
>
|
||||
<ul className="space-y-1.5">
|
||||
{segment.items.map((item, i) => (
|
||||
<li key={i} className="text-xs flex items-start gap-2">
|
||||
<span className="font-mono font-semibold text-green-800 shrink-0">
|
||||
/{item.name}
|
||||
</span>
|
||||
<span className="text-gray-500">{item.description}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CollapsiblePanel>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { StaticVarSegment } from '../../types/protocol'
|
||||
import { Variable } from 'lucide-react'
|
||||
|
||||
export default function StaticVarBadge({ segment }: { segment: StaticVarSegment }) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-blue-100 text-blue-700 text-xs font-mono border border-blue-200">
|
||||
<Variable size={12} />
|
||||
<span className="text-blue-400">{'{{'}{segment.name}{'}}'}</span>
|
||||
<span className="text-blue-300">→</span>
|
||||
<span className="font-semibold">{segment.value}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { SystemPromptSegment } from '../../types/protocol'
|
||||
import CollapsiblePanel from '../CollapsiblePanel'
|
||||
import { Bot } from 'lucide-react'
|
||||
|
||||
export default function SystemPromptView({ segment }: { segment: SystemPromptSegment }) {
|
||||
const lineCount = segment.content.split('\n').length
|
||||
return (
|
||||
<CollapsiblePanel
|
||||
title="System Prompt"
|
||||
icon={<Bot size={16} />}
|
||||
color="border-gray-400 text-gray-600"
|
||||
bgColor="bg-gray-50"
|
||||
defaultCollapsed={segment.collapsed}
|
||||
badge={`${lineCount} 行`}
|
||||
>
|
||||
<pre className="text-xs text-gray-600 whitespace-pre-wrap font-mono leading-relaxed max-h-48 overflow-y-auto">
|
||||
{segment.content}
|
||||
</pre>
|
||||
</CollapsiblePanel>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { TextSegment } from '../../types/protocol'
|
||||
|
||||
export default function TextSegmentView({ segment }: { segment: TextSegment }) {
|
||||
return (
|
||||
<p className="text-gray-800 leading-relaxed whitespace-pre-wrap break-words">
|
||||
{segment.content}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { ToolCallRequestSegment } from '../../types/protocol'
|
||||
import { Play } from 'lucide-react'
|
||||
|
||||
export default function ToolCallRequestView({
|
||||
segment,
|
||||
}: {
|
||||
segment: ToolCallRequestSegment
|
||||
}) {
|
||||
return (
|
||||
<div className="my-2 rounded-lg bg-gray-900 text-gray-100 overflow-hidden border border-gray-700">
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-800">
|
||||
<Play size={14} className="text-green-400" />
|
||||
<span className="text-xs font-mono font-semibold text-green-400">
|
||||
{segment.toolName}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">request</span>
|
||||
</div>
|
||||
<pre className="px-3 py-2 text-xs font-mono text-gray-300 overflow-x-auto">
|
||||
{JSON.stringify(segment.arguments, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useState } from 'react'
|
||||
import type { ToolCallResultSegment } from '../../types/protocol'
|
||||
import { CheckCircle, XCircle, ChevronDown, ChevronRight } from 'lucide-react'
|
||||
|
||||
export default function ToolCallResultView({
|
||||
segment,
|
||||
}: {
|
||||
segment: ToolCallResultSegment
|
||||
}) {
|
||||
const [collapsed, setCollapsed] = useState(segment.collapsed)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`my-2 rounded-lg border-l-4 overflow-hidden ${
|
||||
segment.success
|
||||
? 'border-green-400 bg-green-50'
|
||||
: 'border-red-400 bg-red-50'
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
className="w-full flex items-center gap-2 px-3 py-1.5 text-left hover:opacity-80"
|
||||
>
|
||||
{segment.success ? (
|
||||
<CheckCircle size={14} className="text-green-600" />
|
||||
) : (
|
||||
<XCircle size={14} className="text-red-600" />
|
||||
)}
|
||||
<span className="text-xs font-mono font-semibold text-gray-700">
|
||||
{segment.toolName}
|
||||
</span>
|
||||
<span
|
||||
className={`text-xs ${segment.success ? 'text-green-600' : 'text-red-600'}`}
|
||||
>
|
||||
{segment.success ? '成功' : '失败'}
|
||||
</span>
|
||||
<span className="flex-1" />
|
||||
<span className="text-gray-400">
|
||||
{collapsed ? <ChevronRight size={14} /> : <ChevronDown size={14} />}
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
className={`collapsible-content ${collapsed ? 'collapsed' : 'expanded'}`}
|
||||
>
|
||||
<pre className="px-3 pb-2 text-xs font-mono text-gray-600 whitespace-pre-wrap max-h-48 overflow-y-auto">
|
||||
{segment.result}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useState } from 'react'
|
||||
import type { ToolOverviewSegment } from '../../types/protocol'
|
||||
import CollapsiblePanel from '../CollapsiblePanel'
|
||||
import { Wrench, ChevronDown, ChevronRight, Braces } from 'lucide-react'
|
||||
|
||||
function ToolItemRow({ item }: { item: ToolOverviewSegment['items'][number] }) {
|
||||
const [showSchema, setShowSchema] = useState(false)
|
||||
|
||||
return (
|
||||
<li className="bg-white/60 rounded px-2 py-1.5">
|
||||
<div className="text-xs font-semibold text-orange-800 font-mono">
|
||||
{item.name}
|
||||
<span className="text-orange-400 font-normal">({item.parameters})</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">{item.description}</div>
|
||||
{item.schema && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setShowSchema(!showSchema)}
|
||||
className="flex items-center gap-1 mt-1 text-[10px] text-orange-500 hover:text-orange-700 transition-colors"
|
||||
>
|
||||
<Braces size={10} />
|
||||
JSON Schema
|
||||
{showSchema ? <ChevronDown size={10} /> : <ChevronRight size={10} />}
|
||||
</button>
|
||||
{showSchema && (
|
||||
<pre className="mt-1 text-[10px] text-gray-500 font-mono bg-gray-50 rounded p-1.5 overflow-x-auto max-h-32">
|
||||
{JSON.stringify(item.schema, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ToolOverviewView({ segment }: { segment: ToolOverviewSegment }) {
|
||||
return (
|
||||
<CollapsiblePanel
|
||||
title="Available Tools"
|
||||
icon={<Wrench size={16} />}
|
||||
color="border-orange-400 text-orange-700"
|
||||
bgColor="bg-orange-50"
|
||||
defaultCollapsed={segment.collapsed}
|
||||
badge={`${segment.items.length} tools`}
|
||||
>
|
||||
<ul className="space-y-2">
|
||||
{segment.items.map((item, i) => (
|
||||
<ToolItemRow key={i} item={item} />
|
||||
))}
|
||||
</ul>
|
||||
</CollapsiblePanel>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user