Skip to content

Commit f69daf1

Browse files
authored
Merge pull request #54 from Unity-Lab-AI/codex/fix-image-generation-and-json-formatting
Add JSON repair and structured response handling
2 parents 87864f1 + 378e924 commit f69daf1

8 files changed

Lines changed: 230 additions & 113 deletions

File tree

index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,8 @@ <h3 class="modal-title">Voice Chat</h3>
478478
<script defer src="js/storage/storage.js"></script>
479479
<script defer src="js/storage/memory-api.js"></script>
480480

481+
<!-- JSON repair helpers for AI responses -->
482+
<script type="module" defer src="js/chat/json-repair.js"></script>
481483
<!-- chat-core FIRST so PolliLib's default client helpers are available -->
482484
<script defer src="js/chat/chat-core.js"></script>
483485

js/chat/chat-core.js

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -421,17 +421,60 @@ document.addEventListener("DOMContentLoaded", () => {
421421
});
422422

423423
async function handleToolJson(raw, { imageUrls, audioUrls }) {
424-
try {
425-
const obj = JSON.parse(raw);
424+
const obj = (window.repairJson || (() => ({ text: raw })))(raw);
425+
let handled = false;
426+
427+
if (obj.tool) {
426428
const fn = toolbox.get(obj.tool);
427-
if (!fn) return { handled: false, text: raw };
428-
const res = await fn(obj);
429-
if (res?.imageUrl) imageUrls.push(res.imageUrl);
430-
if (res?.audioUrl) audioUrls.push(res.audioUrl);
431-
return { handled: true, text: res?.text || '' };
432-
} catch {
433-
return { handled: false, text: raw };
429+
if (fn) {
430+
try {
431+
const res = await fn(obj);
432+
if (res?.imageUrl) imageUrls.push(res.imageUrl);
433+
if (res?.audioUrl) audioUrls.push(res.audioUrl);
434+
handled = true;
435+
return { handled: true, text: res?.text || '' };
436+
} catch (e) {
437+
console.warn('tool execution failed', e);
438+
}
439+
}
440+
}
441+
442+
const imgPrompts = obj.image ? [obj.image] : Array.isArray(obj.images) ? obj.images : [];
443+
for (const prompt of imgPrompts) {
444+
if (!(window.polliLib && window.polliClient) || !prompt) continue;
445+
try {
446+
const url = window.polliLib.mcp.generateImageUrl(window.polliClient, { prompt });
447+
imageUrls.push(url);
448+
handled = true;
449+
} catch (e) {
450+
console.warn('polliLib generateImageUrl failed', e);
451+
}
434452
}
453+
454+
const audioText = obj.audio || obj.tts;
455+
if (audioText && window.polliLib && window.polliClient) {
456+
try {
457+
const blob = await window.polliLib.tts(audioText, { model: 'openai-audio' }, window.polliClient);
458+
const url = URL.createObjectURL(blob);
459+
audioUrls.push(url);
460+
handled = true;
461+
} catch (e) {
462+
console.warn('polliLib tts failed', e);
463+
}
464+
}
465+
466+
const command = obj.ui || obj.command;
467+
if (command) {
468+
if (validateUICommand(command)) {
469+
try { executeCommand(command); } catch (e) { console.warn('executeCommand failed', e); }
470+
handled = true;
471+
} else {
472+
console.warn('invalid ui command', command);
473+
}
474+
}
475+
476+
const text = typeof obj.text === 'string' ? obj.text : raw;
477+
return { handled, text };
435478
}
436479

437480
function handleVoiceCommand(text) {

js/chat/json-repair.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export function repairJson(raw) {
2+
if (typeof raw !== 'string') return { text: '' };
3+
let text = raw.trim();
4+
if (!text) return { text: '' };
5+
const attempts = [];
6+
attempts.push(text);
7+
// Attempt: replace single quotes with double quotes
8+
attempts.push(text.replace(/'/g, '"'));
9+
// Attempt: quote unquoted keys, replace single quotes, remove trailing commas
10+
attempts.push(
11+
text
12+
.replace(/([,{]\s*)([A-Za-z0-9_]+)\s*:/g, '$1"$2":')
13+
.replace(/'/g, '"')
14+
.replace(/,\s*([}\]])/g, '$1')
15+
);
16+
for (const str of attempts) {
17+
try {
18+
return JSON.parse(str);
19+
} catch {}
20+
}
21+
// Fallback: treat as plain text
22+
return { text };
23+
}
24+
25+
if (typeof window !== 'undefined') {
26+
window.repairJson = repairJson;
27+
}

js/chat/markdown-sanitizer.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export const defaultBlockedFenceTypes = ['image', 'audio', 'ui'];
2+
3+
export function sanitizeMarkdown(content, blocked = defaultBlockedFenceTypes) {
4+
if (!content) return '';
5+
const pattern = /```(\w+)\n[\s\S]*?```/g;
6+
return content.replace(pattern, (match, type) => {
7+
return blocked.includes(type.toLowerCase()) ? '' : match;
8+
});
9+
}
10+
11+
if (typeof window !== 'undefined') {
12+
window.sanitizeMarkdown = sanitizeMarkdown;
13+
if (!window.blockedFenceTypes) {
14+
window.blockedFenceTypes = [...defaultBlockedFenceTypes];
15+
}
16+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"private": true,
44
"type": "module",
55
"scripts": {
6-
"test": "node tests/pollilib-smoke.mjs && node tests/markdown-sanitization.mjs && node tests/ai-response.mjs && node tests/json-tools.mjs"
6+
"test": "node tests/pollilib-smoke.mjs && node tests/markdown-sanitization.mjs && node tests/ai-response.mjs && node tests/json-tools.mjs && node tests/json-repair.mjs"
77
},
88
"devDependencies": {
99
"marked": "^11.2.0"

prompts/ai-instruct.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,22 +119,30 @@ tell me a joke in a calm tone
119119

120120
## JSON Tools
121121

122-
- As an alternative to fenced blocks, respond with a JSON object to invoke tools.
123-
- The object must include a `tool` field:
124-
- `image` with a `prompt` string to generate an image.
125-
- `tts` with a `text` string for text-to-speech.
126-
- `ui` with a `command` object that follows `docs/ui-command.schema.json`.
127-
- Example:
122+
- As an alternative to fenced blocks, respond with a JSON object.
123+
- The object may include:
124+
- `tool` to invoke a tool (`image`, `tts`, or `ui`).
125+
- `text` for plain responses.
126+
- `image` or `images` with prompt strings to generate images.
127+
- `audio` with text for text-to-speech.
128+
- `command`/`ui` objects that follow `docs/ui-command.schema.json`.
129+
- Examples:
128130

129131
```json
130132
{"tool":"image","prompt":"a glowing neon cityscape at night with flying cars"}
131133
```
132134

133135
```json
134-
{"tool":"ui","command":{"action":"openScreensaver"}}
136+
{"text":"Hello there"}
137+
```
138+
139+
```json
140+
{"images":["a tiny house"],"text":"Here you go"}
135141
```
136142

143+
- Always return valid JSON (double quotes, no trailing commas).
137144
- Do not include extra commentary outside the JSON object.
145+
- If you must send plain text, wrap it as `{ "text": "..." }`.
138146

139147
---
140148

tests/json-repair.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { strict as assert } from 'node:assert';
2+
import { repairJson } from '../js/chat/json-repair.js';
3+
4+
const fixed = repairJson("{tool:'image', prompt:'apple',}");
5+
assert.equal(fixed.tool, 'image');
6+
assert.equal(fixed.prompt, 'apple');
7+
8+
const plain = repairJson('just some text');
9+
assert.deepEqual(plain, { text: 'just some text' });
10+
11+
console.log('json-repair test passed');

0 commit comments

Comments
 (0)