parse LLM chunk fix, alpine store state save/load

frdel committed Dec 2, 2025 at 12:36 UTC a92b2627c8011267a7df7f8ea5971b546d1473a4
2 files changed +78 -2
models.py
+2 -2
@@ -813,7 +813,7 @@ def _parse_chunk(chunk: Any) -> ChatChunk:
813 message.get("content", "")
814 if isinstance(message, dict)
815 else getattr(message, "content", "")
816 - )
816 + ) or ""
817 reasoning_delta = (
818 delta.get("reasoning_content", "")
819 if isinstance(delta, dict)
@@ -822,7 +822,7 @@ def _parse_chunk(chunk: Any) -> ChatChunk:
822 message.get("reasoning_content", "")
823 if isinstance(message, dict)
824 else getattr(message, "reasoning_content", "")
825 - )
825 + ) or ""
826
827 return ChatChunk(reasoning_delta=reasoning_delta, response_delta=response_delta)
828
webui/js/AlpineStore.js
+76
@@ -44,4 +44,80 @@ export function createStore(name, initialState) {
44 */
45 export function getStore(name) {
46 return /** @type {T | undefined} */ (stores.get(name));
47 +}
48 +
49 +/**
50 + * Save current state of a store into a plain object, with optional include/exclude filters.
51 + * If exclude (blacklist) is provided and non-empty, everything except excluded keys is saved.
52 + * Otherwise, if include (whitelist) is provided and non-empty, only included keys are saved.
53 + * If both are empty, all own enumerable properties are saved.
54 + * @param {object} store
55 + * @param {string[]} [include]
56 + * @param {string[]} [exclude]
57 + * @returns {object}
58 + */
59 +export function saveState(store, include = [], exclude = []) {
60 + const hasExclude = Array.isArray(exclude) && exclude.length > 0;
61 + const hasInclude = !hasExclude && Array.isArray(include) && include.length > 0;
62 +
63 + /** @type {Record<string, any>} */
64 + const snapshot = {};
65 +
66 + for (const key of Object.keys(store)) {
67 + if (hasExclude) {
68 + if (exclude.includes(key)) continue;
69 + } else if (hasInclude) {
70 + if (!include.includes(key)) continue;
71 + }
72 +
73 + const value = store[key];
74 + if (typeof value === "function") continue;
75 +
76 + if (Array.isArray(value)) {
77 + snapshot[key] = value.map((item) =>
78 + typeof item === "object" && item !== null ? { ...item } : item
79 + );
80 + } else if (typeof value === "object" && value !== null) {
81 + snapshot[key] = { ...value };
82 + } else {
83 + snapshot[key] = value;
84 + }
85 + }
86 +
87 + return snapshot;
88 +}
89 +
90 +/**
91 + * Load a previously saved state object back into a store, honoring include/exclude filters.
92 + * Filtering rules are the same as in saveState.
93 + * @param {object} store
94 + * @param {object} state
95 + * @param {string[]} [include]
96 + * @param {string[]} [exclude]
97 + */
98 +export function loadState(store, state, include = [], exclude = []) {
99 + if (!state) return;
100 +
101 + const hasExclude = Array.isArray(exclude) && exclude.length > 0;
102 + const hasInclude = !hasExclude && Array.isArray(include) && include.length > 0;
103 +
104 + for (const key of Object.keys(state)) {
105 + if (hasExclude) {
106 + if (exclude.includes(key)) continue;
107 + } else if (hasInclude) {
108 + if (!include.includes(key)) continue;
109 + }
110 +
111 + const value = state[key];
112 +
113 + if (Array.isArray(value)) {
114 + store[key] = value.map((item) =>
115 + typeof item === "object" && item !== null ? { ...item } : item
116 + );
117 + } else if (typeof value === "object" && value !== null) {
118 + store[key] = { ...value };
119 + } else {
120 + store[key] = value;
121 + }
122 + }
123 }
\ No newline at end of file