Skip to content

Commit f112df0

Browse files
committed
feat: enable import character cards from SillyTavern
Add ST card import functionality with PNG/ZIP file upload, character data extraction, and automatic mod generation via LLM. Includes CLI import command, extract preview modal, and batch processing support.
1 parent 561f2f0 commit f112df0

15 files changed

Lines changed: 3249 additions & 304 deletions

.claude/commands/import.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# ST Card Import — CLI Orchestrator
2+
3+
Import SillyTavern character card files (PNG / CharX / ZIP) into the VibeApp system. Extracts apps from the card's character book and generates VibeApp code + mod scenario.
4+
5+
## Parameter Parsing
6+
7+
- `$ARGUMENTS` format: `{FilePath}`
8+
- `FilePath`: path to a `.png`, `.charx`, or `.zip` file containing a SillyTavern character card
9+
10+
If `$ARGUMENTS` is empty, ask the user for the file path.
11+
12+
## Execution Protocol
13+
14+
### 1. Extract Card Data
15+
16+
Run the extraction script:
17+
18+
```bash
19+
python3 .claude/scripts/extract-card.py "{FilePath}"
20+
```
21+
22+
Capture the JSON output. If extraction fails, report the error and stop.
23+
24+
### 2. Analyze & Present Results
25+
26+
Parse the extraction output JSON. Display a structured summary to the user:
27+
28+
```
29+
Card: {source} ({source_type})
30+
Character: {character.name}
31+
Description: {first 100 chars of character.description}...
32+
33+
Apps Found ({count}):
34+
1. [{comment}] — keywords: {keywords} | format: {format} | tags: {tag_names}
35+
2. ...
36+
37+
Lore Entries: {count}
38+
Regex Scripts: {count}
39+
```
40+
41+
### 3. User Selection
42+
43+
Ask the user which apps to generate using AskUserQuestion:
44+
- Option 1: Generate all apps (recommended)
45+
- Option 2: Select specific apps
46+
- Option 3: Skip app generation (mod only)
47+
48+
If the user selects specific apps, present a multi-select list of extracted apps.
49+
50+
### 4. Generate Apps via Vibe Workflow
51+
52+
For each selected app, derive a VibeApp requirement from the card data:
53+
54+
#### 4.1 App Name Derivation
55+
56+
Convert the app's `comment` field to PascalCase for the VibeApp name:
57+
- `"live stream"``LiveStream`
58+
- `"social-feed"``SocialFeed`
59+
- `"music app"``MusicApp`
60+
- Chinese names: translate to English PascalCase
61+
62+
#### 4.2 Requirement Generation
63+
64+
Build a comprehensive requirement description from the extracted data:
65+
66+
```
67+
A {format}-based app that provides {functional description based on keywords and tags}.
68+
69+
UI Features:
70+
{For each tag: describe the UI element it represents}
71+
72+
Data Resources:
73+
{For each resource list: describe what data it manages}
74+
75+
Content Format: {format type — xml tags / bracket notation / prose}
76+
77+
Regex Scripts (for reference):
78+
{List relevant scripts that transform this app's output}
79+
```
80+
81+
#### 4.3 Execute Vibe Workflow
82+
83+
For each app, execute the `/vibe` command:
84+
85+
```
86+
/vibe {PascalCaseAppName} {GeneratedRequirement}
87+
```
88+
89+
Process apps **sequentially** — each vibe workflow must complete before starting the next.
90+
91+
**Important**: Before starting each app, check if a VibeApp with that name already exists at `src/pages/{AppName}/`. If it does, ask the user whether to:
92+
- Skip this app
93+
- Overwrite (delete existing and regenerate)
94+
- Use change mode (modify existing app)
95+
96+
### 5. Completion Report
97+
98+
```
99+
═══════════════════════════════════════
100+
ST Card Import Complete
101+
═══════════════════════════════════════
102+
Source: {filename} ({source_type})
103+
Character: {character.name}
104+
105+
Apps Generated ({count}):
106+
• {AppName1} → http://localhost:3000/{app-name-1}
107+
• {AppName2} → http://localhost:3000/{app-name-2}
108+
═══════════════════════════════════════
109+
```
110+
111+
## Error Handling
112+
113+
- If extraction fails: report error, suggest checking file format
114+
- If a vibe workflow fails for one app: log error, continue with remaining apps
115+
116+
## Notes
117+
118+
- The extraction script handles both PNG (ccv3/chara tEXt chunks) and CharX/ZIP (card.json) formats
119+
- Apps are identified by character book entries containing `<rule S>` in their content
120+
- The vibe workflow handles all code generation, architecture, and integration
121+
- Lore entries are preserved as reference data but not directly used in app generation

apps/webuiapps/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@
1616
"test:coverage": "vitest run --coverage"
1717
},
1818
"dependencies": {
19+
"@anthropic-ai/claude-agent-sdk": "^0.2.72",
1920
"framer-motion": "^12.34.0",
21+
"jszip": "^3.10.1",
2022
"react-markdown": "^10.1.0",
2123
"rehype-raw": "^7.0.0",
2224
"remark-gfm": "^4.0.1"
2325
},
2426
"devDependencies": {
27+
"@types/jszip": "^3.4.1",
2528
"@vitest/coverage-istanbul": "^1.6.1",
2629
"@vitest/coverage-v8": "^1.6.1",
2730
"happy-dom": "^14.0.0",

apps/webuiapps/src/components/ChatPanel/ModPanel.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ interface ModPanelProps {
1616
collection: ModCollection;
1717
onSave: (collection: ModCollection) => void;
1818
onClose: () => void;
19+
initialEditId?: string;
1920
}
2021

21-
const ModPanel: React.FC<ModPanelProps> = ({ collection, onSave, onClose }) => {
22+
const ModPanel: React.FC<ModPanelProps> = ({ collection, onSave, onClose, initialEditId }) => {
2223
const [col, setCol] = useState<ModCollection>(() => ({ ...collection }));
23-
const [editingId, setEditingId] = useState<string | null>(null);
24+
const [editingId, setEditingId] = useState<string | null>(initialEditId ?? null);
2425

2526
const mods = getModList(col);
2627
const activeId = col.activeId;

apps/webuiapps/src/components/ChatPanel/index.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,8 +425,19 @@ const ChatPanel: React.FC<{
425425
const [suggestedReplies, setSuggestedReplies] = useState<string[]>([]);
426426
const [showCharacterPanel, setShowCharacterPanel] = useState(false);
427427
const [showModPanel, setShowModPanel] = useState(false);
428+
const [initialEditModId, setInitialEditModId] = useState<string | undefined>();
428429
const [currentEmotion, setCurrentEmotion] = useState<string | undefined>();
429430

431+
// Auto-open mod editor if redirected from card import
432+
useEffect(() => {
433+
const editModId = sessionStorage.getItem('openroom_edit_mod_id');
434+
if (editModId) {
435+
sessionStorage.removeItem('openroom_edit_mod_id');
436+
setInitialEditModId(editModId);
437+
setShowModPanel(true);
438+
}
439+
}, []);
440+
430441
// Memories loaded for SP injection
431442
const [memories, setMemories] = useState<MemoryEntry[]>([]);
432443

@@ -540,6 +551,20 @@ const ChatPanel: React.FC<{
540551
});
541552
}, []);
542553

554+
// Listen for mod collection changes from Shell (e.g. after mod generation)
555+
useEffect(() => {
556+
const handler = (e: Event) => {
557+
const col = (e as CustomEvent<ModCollection>).detail;
558+
if (col) {
559+
setModCollection(col);
560+
const entry = getActiveModEntry(col);
561+
setModManager(new ModManager(entry.config, entry.state));
562+
}
563+
};
564+
window.addEventListener('mod-collection-changed', handler);
565+
return () => window.removeEventListener('mod-collection-changed', handler);
566+
}, []);
567+
543568
const handleClearHistory = useCallback(async () => {
544569
await clearChatHistory(sessionPathRef.current);
545570
seedPrologue();
@@ -1169,14 +1194,19 @@ const ChatPanel: React.FC<{
11691194
{showModPanel && (
11701195
<ModPanel
11711196
collection={modCollection}
1197+
initialEditId={initialEditModId}
11721198
onSave={(col) => {
11731199
setModCollection(col);
11741200
saveModCollection(col);
11751201
const entry = getActiveModEntry(col);
11761202
setModManager(new ModManager(entry.config, entry.state));
11771203
setShowModPanel(false);
1204+
setInitialEditModId(undefined);
1205+
}}
1206+
onClose={() => {
1207+
setShowModPanel(false);
1208+
setInitialEditModId(undefined);
11781209
}}
1179-
onClose={() => setShowModPanel(false)}
11801210
/>
11811211
)}
11821212
</>

0 commit comments

Comments
 (0)