Skip to content

Commit 561f2f0

Browse files
committed
fix(diary): support markdown rendering in preview mode and strengthen LLM file path rules
- Rewrite Diary preview renderer to use react-markdown + remark-gfm + rehype-raw, enabling proper rendering of headings, bold, italic, tables, code blocks, etc. - Preserve custom {{strike}}/{{scribble}}/{{messy}} hand-drawn SVG effects via rehype-raw passthrough with data-effect attributes. - Add stripMarkdown utility for sidebar list preview to show clean plain text. - Add comprehensive markdown element styles to .readContent matching the handwritten diary aesthetic (including GFM table styling). - Strengthen ChatPanel system prompt: require LLM to read guide.md before any file_write, forbid inventing file paths, enforce full subdirectory structure from guide.md to prevent data written to wrong locations.
1 parent 0118623 commit 561f2f0

File tree

12 files changed

+915
-98
lines changed

12 files changed

+915
-98
lines changed

apps/webuiapps/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
"test:coverage": "vitest run --coverage"
1717
},
1818
"dependencies": {
19-
"framer-motion": "^12.34.0"
19+
"framer-motion": "^12.34.0",
20+
"react-markdown": "^10.1.0",
21+
"rehype-raw": "^7.0.0",
22+
"remark-gfm": "^4.0.1"
2023
},
2124
"devDependencies": {
2225
"@vitest/coverage-istanbul": "^1.6.1",

apps/webuiapps/src/components/ChatPanel/index.module.scss

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
border-left: 1px solid rgba(255, 255, 255, 0.06);
99
display: flex;
1010
flex-direction: row;
11-
z-index: 9998;
1211
}
1312

1413
/* ---- Left: Character Avatar ---- */
@@ -303,7 +302,7 @@
303302
display: flex;
304303
align-items: center;
305304
justify-content: center;
306-
z-index: 10000;
305+
z-index: 100000;
307306
}
308307

309308
.settingsModal {

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

Lines changed: 110 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useRef, useEffect, useCallback } from 'react';
1+
import React, { useState, useRef, useEffect, useCallback, memo } from 'react';
22
import {
33
Settings,
44
Trash2,
@@ -195,7 +195,9 @@ Rules:
195195
- Data mutations MUST go through file_write/file_delete. app_action only notifies the app to reload, it cannot write data.
196196
- After file_write, ALWAYS call app_action with the corresponding REFRESH action.
197197
- Do NOT skip step 5. If the user asked to save/create/add something, you must file_write the data. file_list alone does not save anything.
198-
- NAS paths in guide.md like "/articles/xxx.json" map to "apps/{appName}/data/articles/xxx.json".
198+
- Do NOT skip steps 2-3. You MUST read guide.md before ANY file_write. The guide defines the ONLY valid directory structure and file schemas. Writing to paths not defined in guide.md will cause data loss — the app will not see the files.
199+
- NEVER invent or guess file paths. ALL file_write paths MUST exactly follow the directory structure in guide.md. For example, if guide.md defines entries under "/entries/{id}.json", you MUST write to "apps/{appName}/data/entries/{id}.json" — NOT to "apps/{appName}/data/{id}.json" or any other path.
200+
- NAS paths in guide.md like "/articles/xxx.json" map to "apps/{appName}/data/articles/xxx.json". This prefix rule applies to ALL paths — always preserve the full subdirectory structure from guide.md.
199201
200202
When you receive "[User performed action in ... (appName: xxx)]", the appName is already provided. Read its meta.yaml to understand available actions, then respond accordingly. For games, respond with your own move — think strategically.
201203
@@ -284,14 +286,105 @@ const ActionsTaken: React.FC<{ calls: string[] }> = ({ calls }) => {
284286
);
285287
};
286288

289+
// ---------------------------------------------------------------------------
290+
// CharacterAvatar – crossfade between emotion media without flashing
291+
// ---------------------------------------------------------------------------
292+
293+
interface AvatarLayer {
294+
url: string;
295+
type: 'video' | 'image';
296+
active: boolean;
297+
}
298+
299+
const CharacterAvatar: React.FC<{
300+
character: CharacterConfig;
301+
emotion?: string;
302+
onEmotionEnd: () => void;
303+
}> = memo(({ character, emotion, onEmotionEnd }) => {
304+
const isIdle = !emotion;
305+
const media = resolveEmotionMedia(character, emotion || 'default');
306+
307+
const [layers, setLayers] = useState<AvatarLayer[]>(() =>
308+
media ? [{ url: media.url, type: media.type, active: true }] : [],
309+
);
310+
const activeUrl = layers.find((l) => l.active)?.url;
311+
312+
useEffect(() => {
313+
if (!media) {
314+
setLayers([]);
315+
return;
316+
}
317+
if (media.url === activeUrl) return;
318+
setLayers((prev) => {
319+
if (prev.some((l) => l.url === media.url)) return prev;
320+
return [...prev, { url: media.url, type: media.type, active: false }];
321+
});
322+
}, [media?.url, activeUrl]);
323+
324+
const handleMediaReady = useCallback((readyUrl: string) => {
325+
setLayers((prev) => {
326+
const staleUrls = prev.filter((l) => l.url !== readyUrl).map((l) => l.url);
327+
setTimeout(() => {
328+
setLayers((curr) => curr.filter((l) => !staleUrls.includes(l.url)));
329+
}, 300);
330+
return prev.map((l) => ({ ...l, active: l.url === readyUrl }));
331+
});
332+
}, []);
333+
334+
if (layers.length === 0) {
335+
return <div className={styles.avatarPlaceholder}>{character.character_name.charAt(0)}</div>;
336+
}
337+
338+
return (
339+
<>
340+
{layers.map((layer) => {
341+
const layerStyle: React.CSSProperties = {
342+
position: 'absolute',
343+
inset: 0,
344+
opacity: layer.active ? 1 : 0,
345+
transition: 'opacity 0.25s ease-out',
346+
};
347+
if (layer.type === 'video') {
348+
return (
349+
<video
350+
key={layer.url}
351+
className={styles.avatarImage}
352+
style={layerStyle}
353+
src={layer.url}
354+
autoPlay
355+
loop={layer.active ? isIdle : false}
356+
muted
357+
playsInline
358+
onCanPlay={!layer.active ? () => handleMediaReady(layer.url) : undefined}
359+
onEnded={layer.active && !isIdle ? onEmotionEnd : undefined}
360+
/>
361+
);
362+
}
363+
return (
364+
<img
365+
key={layer.url}
366+
className={styles.avatarImage}
367+
style={layerStyle}
368+
src={layer.url}
369+
alt={character.character_name}
370+
onLoad={!layer.active ? () => handleMediaReady(layer.url) : undefined}
371+
/>
372+
);
373+
})}
374+
</>
375+
);
376+
});
377+
287378
// ---------------------------------------------------------------------------
288379
// ChatPanel
289380
// ---------------------------------------------------------------------------
290381

291-
const ChatPanel: React.FC<{ onClose: () => void; visible?: boolean }> = ({
292-
onClose,
293-
visible = true,
294-
}) => {
382+
const ChatPanel: React.FC<{
383+
onClose: () => void;
384+
visible?: boolean;
385+
zIndex?: number;
386+
onFocus?: () => void;
387+
}> = ({ onClose, visible = true, zIndex, onFocus }) => {
295388
// Character + Mod state (collection-based)
296389
const [charCollection, setCharCollection] = useState<CharacterCollection>(
297390
() => loadCharacterCollectionSync() ?? DEFAULT_CHAR_COLLECTION,
@@ -915,37 +1008,19 @@ const ChatPanel: React.FC<{ onClose: () => void; visible?: boolean }> = ({
9151008

9161009
return (
9171010
<>
918-
<div className={styles.panel} data-testid="chat-panel">
1011+
<div
1012+
className={styles.panel}
1013+
data-testid="chat-panel"
1014+
style={zIndex !== null && zIndex !== undefined ? { zIndex } : undefined}
1015+
onMouseDown={onFocus}
1016+
>
9191017
{/* Left: Character Avatar */}
9201018
<div className={styles.avatarSide}>
921-
{(() => {
922-
// Resolve media for the active emotion, or fall back to "peaceful" as idle
923-
const idleEmotion = 'default';
924-
const activeEmotion = currentEmotion || idleEmotion;
925-
const isIdle = !currentEmotion;
926-
const media = resolveEmotionMedia(character, activeEmotion);
927-
928-
if (!media) {
929-
return (
930-
<div className={styles.avatarPlaceholder}>{character.character_name.charAt(0)}</div>
931-
);
932-
}
933-
934-
return media.type === 'video' ? (
935-
<video
936-
key={media.url}
937-
className={styles.avatarImage}
938-
src={media.url}
939-
autoPlay
940-
loop={isIdle}
941-
muted
942-
playsInline
943-
onEnded={isIdle ? undefined : () => setCurrentEmotion(undefined)}
944-
/>
945-
) : (
946-
<img className={styles.avatarImage} src={media.url} alt={character.character_name} />
947-
);
948-
})()}
1019+
<CharacterAvatar
1020+
character={character}
1021+
emotion={currentEmotion}
1022+
onEmotionEnd={() => setCurrentEmotion(undefined)}
1023+
/>
9491024
</div>
9501025

9511026
{/* Right: Chat */}

apps/webuiapps/src/components/ChatPanel/panel.module.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
display: flex;
88
align-items: center;
99
justify-content: center;
10-
z-index: 10000;
10+
z-index: 100000;
1111
}
1212

1313
.panel {

apps/webuiapps/src/components/Shell/index.module.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
height: 280px;
1717
border-radius: 16px;
1818
overflow: hidden;
19-
z-index: 10001;
19+
z-index: 100001;
2020
box-shadow:
2121
0 8px 32px rgba(0, 0, 0, 0.6),
2222
0 0 0 1px rgba(255, 255, 255, 0.12);
@@ -160,7 +160,7 @@
160160
display: flex;
161161
gap: 12px;
162162
align-items: center;
163-
z-index: 9999;
163+
z-index: 99999;
164164
padding: 8px 16px;
165165
border-radius: 24px;
166166
background: rgba(0, 0, 0, 0.45);

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
} from 'lucide-react';
2020
import ChatPanel from '../ChatPanel';
2121
import AppWindow from '../AppWindow';
22-
import { getWindows, subscribe, openWindow } from '@/lib/windowManager';
22+
import { getWindows, subscribe, openWindow, claimZIndex } from '@/lib/windowManager';
2323
import { getDesktopApps } from '@/lib/appRegistry';
2424
import { reportUserOsAction, onOSEvent } from '@/lib/vibeContainerMock';
2525
import { setReportUserActions } from '@/lib';
@@ -74,6 +74,7 @@ const Shell: React.FC = () => {
7474
const [liveWallpaper, setLiveWallpaper] = useState(false);
7575
// eslint-disable-next-line @typescript-eslint/no-unused-vars
7676
const [wallpaper, setWallpaper] = useState(VIDEO_WALLPAPER);
77+
const [chatZIndex, setChatZIndex] = useState(() => claimZIndex());
7778
const [pipPos, setPipPos] = useState<{ x: number; y: number } | null>(null);
7879
const dragRef = useRef<{ startX: number; startY: number; origX: number; origY: number } | null>(
7980
null,
@@ -209,7 +210,12 @@ const Shell: React.FC = () => {
209210
))}
210211

211212
{/* Chat Panel — always mounted to preserve chat history */}
212-
<ChatPanel onClose={() => setChatOpen(false)} visible={chatOpen} />
213+
<ChatPanel
214+
onClose={() => setChatOpen(false)}
215+
visible={chatOpen}
216+
zIndex={chatZIndex}
217+
onFocus={() => setChatZIndex(claimZIndex())}
218+
/>
213219

214220
<div ref={barRef} className={`${styles.bottomBar} ${chatOpen ? styles.chatOpen : ''}`}>
215221
<button

0 commit comments

Comments
 (0)