Fix remaining code-review findings (15 bugs; #11 skipped as AID-compatible)
Backend: - provider: fall back to parsing a plain JSON body when a server ignores stream=true (was: silent empty turn); error if response has no text (#5) - memorybank: clamp cursors after undo/retry shrinks the action list, and translate summary_cursor (list position) to an Action.index boundary before comparing with Memory.source_end (#7) - memorybank: pinned memories now count toward the top_k budget (#8) - memorybank: cosine() returns 0.0 on dimension mismatch; changing the embedding model clears stored vectors so they re-embed (#9) - scripting: MAX_STORY_CARDS cap now counts cards inserted during the hook, so a script can't add unbounded cards in one turn (#10) - settings: /test tolerates non-dict JSON from /models (#13) - scenarios: import accepts worldInformation as a story-card source (#14) Frontend: - per-key debounce timers in PlotPanel and ScenarioEditor — editing two things within 600ms no longer drops the first save (#15, #16) - Continue button no longer discards typed input (#17) - failed retry resyncs actions from the server instead of leaving the removed action missing (#18) - Settings save/test surface errors instead of hanging on Testing… (#19) - InsightsPanel ignores stale responses from superseded requests (#20) - placeholder scan includes story-card trigger keys (#21) addStoryCard returning the 0-based index (falsy for the first card) matches real AI Dungeon per the scripting guidebook — kept, documented (#11). Statuses updated in CODE_REVIEW_FINDINGS.md; stale entries for previously fixed items (#1-4, #6, #12) corrected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KFsGHju9szibJJa2YJcdbg
This commit is contained in:
co-authored by
Claude Fable 5
parent
3ee653a631
commit
253b533d3b
@@ -50,12 +50,17 @@ const SECTION_LABELS = {
|
||||
}
|
||||
|
||||
function PlotPanel({ adventure, setAdventure }) {
|
||||
const saveTimer = useRef(null)
|
||||
// One timer per field/card: a single shared timer would cancel the pending
|
||||
// save of whatever was edited previously within the debounce window.
|
||||
const saveTimers = useRef(new Map())
|
||||
const debounceSave = (key, fn) => {
|
||||
clearTimeout(saveTimers.current.get(key))
|
||||
saveTimers.current.set(key, setTimeout(fn, 600))
|
||||
}
|
||||
|
||||
const setField = (field, value) => {
|
||||
setAdventure({ ...adventure, [field]: value })
|
||||
clearTimeout(saveTimer.current)
|
||||
saveTimer.current = setTimeout(() => api.updateAdventure(adventure.id, { [field]: value }), 600)
|
||||
debounceSave(field, () => api.updateAdventure(adventure.id, { [field]: value }))
|
||||
}
|
||||
|
||||
const addCard = async () => {
|
||||
@@ -68,12 +73,11 @@ function PlotPanel({ adventure, setAdventure }) {
|
||||
...adventure,
|
||||
story_cards: adventure.story_cards.map((c) => (c.id === card.id ? card : c)),
|
||||
})
|
||||
clearTimeout(saveTimer.current)
|
||||
saveTimer.current = setTimeout(() => {
|
||||
debounceSave(`card-${card.id}`, () => {
|
||||
api.updateStoryCard(card.id, {
|
||||
name: card.name, type: card.type, keys: card.keys, entry: card.entry, notes: card.notes,
|
||||
})
|
||||
}, 600)
|
||||
})
|
||||
}
|
||||
|
||||
const deleteCard = async (cardId) => {
|
||||
@@ -324,11 +328,15 @@ function InsightsPanel({ advId, inspectActionId, onClearInspect, refreshKey }) {
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
let stale = false // a slow earlier request must not clobber a newer one
|
||||
setError(null)
|
||||
const load = inspectActionId
|
||||
? api.getActionContext(advId, inspectActionId)
|
||||
: api.getAdventureContext(advId)
|
||||
load.then(setReport).catch((err) => { setReport(null); setError(err.message) })
|
||||
load
|
||||
.then((r) => { if (!stale) setReport(r) })
|
||||
.catch((err) => { if (!stale) { setReport(null); setError(err.message) } })
|
||||
return () => { stale = true }
|
||||
}, [advId, inspectActionId, refreshKey])
|
||||
|
||||
if (error) return <div className="empty">{error}</div>
|
||||
@@ -502,6 +510,11 @@ export default function Play() {
|
||||
|
||||
function send(type = mode) {
|
||||
const text = input.trim()
|
||||
if (type === 'continue') {
|
||||
// Continue never consumes typed text — leave it in the box.
|
||||
runTurn((signal) => api.sendAction(id, { type: 'continue', text: '' }, handleEvent, signal))
|
||||
return
|
||||
}
|
||||
const payload = { type: text ? type : 'continue', text }
|
||||
setInput('')
|
||||
runTurn((signal) => api.sendAction(id, payload, handleEvent, signal))
|
||||
@@ -510,7 +523,16 @@ export default function Play() {
|
||||
function retry() {
|
||||
setActions((prev) =>
|
||||
prev.length && prev[prev.length - 1].type === 'ai' ? prev.slice(0, -1) : prev)
|
||||
runTurn((signal) => api.retry(id, handleEvent, signal))
|
||||
runTurn(async (signal) => {
|
||||
try {
|
||||
await api.retry(id, handleEvent, signal)
|
||||
} catch (err) {
|
||||
// Failed retry (409, network): the optimistically removed action may
|
||||
// still exist server-side — resync instead of guessing.
|
||||
api.getAdventure(id).then((adv) => setActions(adv.actions)).catch(() => {})
|
||||
throw err
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function undo() {
|
||||
|
||||
@@ -9,7 +9,13 @@ export default function ScenarioEditor() {
|
||||
const [scenario, setScenario] = useState(null)
|
||||
const [allScripts, setAllScripts] = useState([])
|
||||
const [status, setStatus] = useState('')
|
||||
const saveTimer = useRef(null)
|
||||
// One timer per field/card: a single shared timer would cancel the pending
|
||||
// save of whatever was edited previously within the debounce window.
|
||||
const saveTimers = useRef(new Map())
|
||||
const debounceSave = (key, fn) => {
|
||||
clearTimeout(saveTimers.current.get(key))
|
||||
saveTimers.current.set(key, setTimeout(fn, 600))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
api.getScenario(id).then(setScenario).catch(() => navigate('/scenarios'))
|
||||
@@ -19,12 +25,11 @@ export default function ScenarioEditor() {
|
||||
const setField = (field, value) => {
|
||||
const next = { ...scenario, [field]: value }
|
||||
setScenario(next)
|
||||
clearTimeout(saveTimer.current)
|
||||
saveTimer.current = setTimeout(async () => {
|
||||
debounceSave(field, async () => {
|
||||
await api.updateScenario(id, { [field]: value })
|
||||
setStatus('Saved')
|
||||
setTimeout(() => setStatus(''), 1500)
|
||||
}, 600)
|
||||
})
|
||||
}
|
||||
|
||||
const addCard = async () => {
|
||||
@@ -37,12 +42,11 @@ export default function ScenarioEditor() {
|
||||
...scenario,
|
||||
story_cards: scenario.story_cards.map((c) => (c.id === card.id ? card : c)),
|
||||
})
|
||||
clearTimeout(saveTimer.current)
|
||||
saveTimer.current = setTimeout(() => {
|
||||
debounceSave(`card-${card.id}`, () => {
|
||||
api.updateStoryCard(card.id, {
|
||||
name: card.name, type: card.type, keys: card.keys, entry: card.entry, notes: card.notes,
|
||||
})
|
||||
}, 600)
|
||||
})
|
||||
}
|
||||
|
||||
const deleteCard = async (cardId) => {
|
||||
|
||||
@@ -49,7 +49,8 @@ export default function Scenarios() {
|
||||
const scenario = await api.getScenario(scenarioId)
|
||||
const names = extractPlaceholders(
|
||||
scenario.prompt, scenario.memory, scenario.authors_note, scenario.ai_instructions,
|
||||
...scenario.story_cards.map((c) => c.entry),
|
||||
// Cards can carry ${placeholders} in trigger keys too, not just entries.
|
||||
...scenario.story_cards.flatMap((c) => [c.keys, c.entry]),
|
||||
)
|
||||
if (names.length === 0) return begin(scenarioId)
|
||||
setPending({ scenario, names })
|
||||
|
||||
@@ -56,16 +56,23 @@ export default function Settings() {
|
||||
const setField = (field, value) => setSettings({ ...settings, [field]: value })
|
||||
|
||||
const save = async () => {
|
||||
await api.updateSettings(settings)
|
||||
setSaved('Settings saved')
|
||||
setTimeout(() => setSaved(''), 2000)
|
||||
try {
|
||||
await api.updateSettings(settings)
|
||||
setSaved('Settings saved')
|
||||
} catch (err) {
|
||||
setSaved(`Save failed: ${err.message}`)
|
||||
}
|
||||
setTimeout(() => setSaved(''), 4000)
|
||||
}
|
||||
|
||||
const test = async () => {
|
||||
setTestResult({ pending: true })
|
||||
await api.updateSettings(settings)
|
||||
const result = await api.testConnection()
|
||||
setTestResult(result)
|
||||
try {
|
||||
await api.updateSettings(settings)
|
||||
setTestResult(await api.testConnection())
|
||||
} catch (err) {
|
||||
setTestResult({ ok: false, detail: err.message })
|
||||
}
|
||||
}
|
||||
|
||||
if (!settings) return null
|
||||
|
||||
Reference in New Issue
Block a user