Skip to content

Commit cfcb3c2

Browse files
authored
Merge pull request #19 from creasexul/feat/st-card-import
feat: enable import character cards from SillyTavern
2 parents 561f2f0 + 8daad37 commit cfcb3c2

File tree

14 files changed

+2687
-5
lines changed

14 files changed

+2687
-5
lines changed

.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

.claude/prompt_generate_mod_en.md

Lines changed: 135 additions & 0 deletions
Large diffs are not rendered by default.

apps/webuiapps/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
},
1818
"dependencies": {
1919
"framer-motion": "^12.34.0",
20+
"jszip": "^3.10.1",
2021
"react-markdown": "^10.1.0",
2122
"rehype-raw": "^7.0.0",
2223
"remark-gfm": "^4.0.1"
2324
},
2425
"devDependencies": {
26+
"@anthropic-ai/claude-agent-sdk": "^0.2.72",
2527
"@vitest/coverage-istanbul": "^1.6.1",
2628
"@vitest/coverage-v8": "^1.6.1",
2729
"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: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,8 +425,22 @@ 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+
// Open mod editor when triggered from Shell (e.g. after card import mod generation)
432+
useEffect(() => {
433+
const handler = (e: Event) => {
434+
const modId = (e as CustomEvent<{ modId: string }>).detail?.modId;
435+
if (modId) {
436+
setInitialEditModId(modId);
437+
setShowModPanel(true);
438+
}
439+
};
440+
window.addEventListener('open-mod-editor', handler);
441+
return () => window.removeEventListener('open-mod-editor', handler);
442+
}, []);
443+
430444
// Memories loaded for SP injection
431445
const [memories, setMemories] = useState<MemoryEntry[]>([]);
432446

@@ -540,6 +554,20 @@ const ChatPanel: React.FC<{
540554
});
541555
}, []);
542556

557+
// Listen for mod collection changes from Shell (e.g. after mod generation)
558+
useEffect(() => {
559+
const handler = (e: Event) => {
560+
const col = (e as CustomEvent<ModCollection>).detail;
561+
if (col) {
562+
setModCollection(col);
563+
const entry = getActiveModEntry(col);
564+
setModManager(new ModManager(entry.config, entry.state));
565+
}
566+
};
567+
window.addEventListener('mod-collection-changed', handler);
568+
return () => window.removeEventListener('mod-collection-changed', handler);
569+
}, []);
570+
543571
const handleClearHistory = useCallback(async () => {
544572
await clearChatHistory(sessionPathRef.current);
545573
seedPrologue();
@@ -1169,14 +1197,19 @@ const ChatPanel: React.FC<{
11691197
{showModPanel && (
11701198
<ModPanel
11711199
collection={modCollection}
1200+
initialEditId={initialEditModId}
11721201
onSave={(col) => {
11731202
setModCollection(col);
11741203
saveModCollection(col);
11751204
const entry = getActiveModEntry(col);
11761205
setModManager(new ModManager(entry.config, entry.state));
11771206
setShowModPanel(false);
1207+
setInitialEditModId(undefined);
1208+
}}
1209+
onClose={() => {
1210+
setShowModPanel(false);
1211+
setInitialEditModId(undefined);
11781212
}}
1179-
onClose={() => setShowModPanel(false)}
11801213
/>
11811214
)}
11821215
</>

0 commit comments

Comments
 (0)