File size: 9,789 Bytes
2dd2827
 
 
8e22657
c3124a7
2dd2827
3446746
 
 
 
c03ed1e
2dd2827
c3124a7
 
 
 
8e22657
b8d04a8
 
50ca5e6
89ca1d8
 
 
 
8e22657
afb854c
 
3446746
 
 
afb854c
 
4622d79
89ca1d8
afb854c
 
 
 
 
8e22657
 
 
73d7676
3446746
 
 
c3124a7
 
3446746
37ba9ab
3446746
8e22657
 
afb854c
8e22657
 
 
57fd4b1
8e22657
 
89ca1d8
 
 
37ba9ab
89ca1d8
 
afb854c
c5dac88
3446746
 
 
 
 
 
 
 
 
 
 
afb854c
c5dac88
 
3446746
afb854c
110db83
3446746
 
 
 
 
 
 
 
 
 
 
 
 
afb854c
3446746
c5dac88
3446746
 
 
bb94fa2
afb854c
3446746
 
 
 
 
a7a7ca5
 
3446746
bb94fa2
d27bfbf
3446746
 
 
 
 
d27bfbf
3446746
a94f360
3446746
 
a94f360
3446746
d27bfbf
 
bb94fa2
d27bfbf
 
3446746
 
 
 
 
bb94fa2
 
 
 
 
73d7676
37ba9ab
3446746
 
 
 
 
73d7676
9f50718
73d7676
9f50718
 
 
 
afb854c
9f50718
 
 
 
5fa1c4d
9f50718
 
 
5fa1c4d
 
9f50718
 
5fa1c4d
 
 
 
 
 
 
 
 
 
 
 
afb854c
9f50718
73d7676
37ba9ab
 
3446746
06bf72a
8e22657
9f50718
37ba9ab
 
c5dac88
 
37ba9ab
 
8e22657
10bad20
 
c3124a7
 
10bad20
 
 
 
 
3446746
355174b
3446746
 
 
 
 
 
 
10bad20
5fa1c4d
3446746
 
 
 
 
355174b
3446746
afb854c
3446746
afb854c
3446746
6b4e1b3
 
10bad20
 
6b4e1b3
afb854c
 
 
 
 
 
 
3446746
afb854c
3446746
 
 
 
 
 
 
 
afb854c
 
 
 
c5dac88
3446746
8e22657
50ca5e6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
import { createClient } from '@supabase/supabase-js';

let supabase = null;
const activeProjects = new Map();
const initializationLocks = new Set(); 

// --- REALTIME BUFFERS ---
const streamBuffers = new Map(); // Destructive (Plugin)
const statusBuffers = new Map(); // Status (Frontend)
const snapshotBuffers = new Map(); // Non-Destructive (Frontend)

export const initDB = () => {
    if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_ROLE_KEY) {
        console.error("Missing Supabase Env Variables");
        return;
    }
    supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY);
};

export const StateManager = {
    isLocked: (projectId) => initializationLocks.has(projectId),
    lock: (projectId) => initializationLocks.add(projectId),
    unlock: (projectId) => initializationLocks.delete(projectId),

    getProject: async (projectId) => {
        if (activeProjects.has(projectId)) {
            const cached = activeProjects.get(projectId);
            if (cached.workerHistory && cached.pmHistory) {
                return cached;
            }
        }
        
        const { data: proj, error } = await supabase.from('projects').select('*').eq('id', projectId).single();
        if (error || !proj) return null;

        const { data: chunks } = await supabase.from('message_chunks')
            .select('*').eq('project_id', projectId)
            .order('chunk_index', { ascending: false }).limit(10);

        const memoryObject = {
            ...proj.info,
            id: proj.id,
            userId: proj.user_id,
            thumbnail: proj.info?.thumbnail || null,
            gdd: proj.info?.gdd || null,
            
            workerHistory: (chunks || []).filter(c => c.type === 'worker').reverse().flatMap(c => c.payload || []),
            pmHistory: (chunks || []).filter(c => c.type === 'pm').reverse().flatMap(c => c.payload || []),
            
            commandQueue: [], 
            failureCount: proj.info?.failureCount || 0,
            lastActive: Date.now()
        };

        activeProjects.set(projectId, memoryObject);
        return memoryObject;
    },

    addHistory: async (projectId, type, role, text) => {
        const newMessage = { role, parts: [{ text }] };
        const project = activeProjects.get(projectId);
        if (project) {
            const historyKey = type === 'pm' ? 'pmHistory' : 'workerHistory';
            if (!Array.isArray(project[historyKey])) project[historyKey] = [];
            project[historyKey].push(newMessage);
        }

        try {
            const { data: chunks, error: fetchError } = await supabase.from('message_chunks')
                .select('id, chunk_index, payload')
                .eq('project_id', projectId)
                .eq('type', type)
                .order('chunk_index', { ascending: false })
                .limit(10);

            if (fetchError) {
                console.error(`[DB Error] Failed to fetch history for ${projectId}:`, fetchError.message);
                return;
            }

            const latest = chunks?.[0];
            const currentPayload = (latest && Array.isArray(latest.payload)) ? latest.payload : [];
            const latestIndex = (latest && typeof latest.chunk_index === 'number') ? latest.chunk_index : -1;

            if (latest && currentPayload.length < 20) {
                const updatedPayload = [...currentPayload, newMessage];
                const { error: updateError } = await supabase.from('message_chunks')
                    .update({ payload: updatedPayload })
                    .eq('id', latest.id);
                if (updateError) console.error(`[DB Error] Update chunk failed:`, updateError.message);
            } 
            else {
                const nextIndex = latestIndex + 1;
                const { error: insertError } = await supabase.from('message_chunks').insert({
                    project_id: projectId,
                    type,
                    chunk_index: nextIndex,
                    payload: [newMessage]
                });
                if (insertError) console.error(`[DB Error] Insert new chunk failed:`, insertError.message);
            }
        } catch (e) {
            console.error("[StateManager] Unexpected error in addHistory:", e);
        }
    },

    setStatus: (projectId, status) => {
        statusBuffers.set(projectId, status);
    },

    getStatus: (projectId) => {
        // FIX: Default to "Idle" instead of "Working..." to prevent plugin lockup
        return statusBuffers.get(projectId) || "Idle";
    },

    appendStream: (projectId, chunk) => {
        const currentDestructive = streamBuffers.get(projectId) || "";
        streamBuffers.set(projectId, currentDestructive + chunk);

        const currentSnapshot = snapshotBuffers.get(projectId) || "";
        snapshotBuffers.set(projectId, currentSnapshot + chunk);
    },

    appendSnapshotOnly: (projectId, chunk) => {
        const currentSnapshot = snapshotBuffers.get(projectId) || "";
        snapshotBuffers.set(projectId, currentSnapshot + chunk);
    },

    popStream: (projectId) => {
        const content = streamBuffers.get(projectId) || "";
        streamBuffers.set(projectId, ""); 
        return content;
    },

    getSnapshot: (projectId) => {
        return snapshotBuffers.get(projectId) || "";
    },

    clearSnapshot: (projectId) => {
        snapshotBuffers.delete(projectId);
        statusBuffers.delete(projectId);
    },

    queueCommand: async (projectId, input) => {
        let project = activeProjects.get(projectId);
        
        if (!project) {
            project = await StateManager.getProject(projectId);
        }

        if (!project) return;

        let command = null;

        if (typeof input === 'object' && input.type && input.payload) {
            command = input;
        } 
        else if (typeof input === 'string') {
            const rawResponse = input;
            
            if (rawResponse.includes("[ASK_PM:")) return;
            if (rawResponse.includes("[ROUTE_TO_PM:")) return;
            // Only skip image generation if there's no code block, otherwise logic flow handles it
            if (rawResponse.includes("[GENERATE_IMAGE:") && !rawResponse.includes("```")) return;

            const codeMatch = rawResponse.match(/```(?:lua|luau)?([\s\S]*?)```/i);
            const readScriptMatch = rawResponse.match(/\[READ_SCRIPT:\s*(.*?)\]/i);
            const readHierarchyMatch = rawResponse.match(/\[READ_HIERARCHY(?::\s*(.*?))?\]/i);
            const readLogsMatch = rawResponse.includes("[READ_LOGS]");

            if (codeMatch) {
                command = { type: "EXECUTE", payload: codeMatch[1].trim() };
            } 
            else if (readScriptMatch) {
                command = { type: "READ_SCRIPT", payload: readScriptMatch[1].trim() };
            } 
            else if (readHierarchyMatch) {
                command = { type: "READ_HIERARCHY", payload: readHierarchyMatch[1] ? readHierarchyMatch[1].trim() : "All" };
            } 
            else if (readLogsMatch) {
                command = { type: "READ_LOGS", payload: null };
            }
        }
        
        if (command) {
            if (!project.commandQueue) project.commandQueue = [];
            project.commandQueue.push(command);
            console.log(`[Memory] Queued command for ${projectId}: ${command.type}`);
        }
    },

    popCommand: async (projectId) => {
        const project = activeProjects.get(projectId);
        if (!project || !project.commandQueue || project.commandQueue.length === 0) return null;
        return project.commandQueue.shift();
    },

    updateProject: async (projectId, data) => {
        const now = new Date().toISOString(); 

        if (activeProjects.has(projectId)) {
            const current = activeProjects.get(projectId);
            const newData = { 
                ...current, 
                ...data, 
                lastActive: Date.now() 
            };
            activeProjects.set(projectId, newData);
        }

        const payload = {
            info: {
                title: data.title,
                status: data.status,
                stats: data.stats,
                description: data.description,
                failureCount: data.failureCount,
                last_edited: now 
            }
        };

        Object.keys(payload.info).forEach(key => payload.info[key] === undefined && delete payload.info[key]);

        const { data: currentDb } = await supabase.from('projects').select('info').eq('id', projectId).single();
        
        if (currentDb) {
             const mergedInfo = { ...currentDb.info, ...payload.info };
             delete mergedInfo.commandQueue; 
             
             const { error: infoError } = await supabase.from('projects')
                .update({ info: mergedInfo })
                .eq('id', projectId);
             
             if (infoError) console.error("[DB ERROR] Update Project Info failed:", infoError.message);
        }
    },

    cleanupMemory: () => {
        const now = Date.now();
        const FOUR_HOURS = 4 * 60 * 60 * 1000;
        let count = 0;

        for (const [id, data] of activeProjects.entries()) {
            if (!data.lastActive) data.lastActive = now; 

            if (now - data.lastActive > FOUR_HOURS) {
                console.log(`[StateManager] 🧹 Removing expired project: ${id}`);
                activeProjects.delete(id);
                streamBuffers.delete(id); 
                snapshotBuffers.delete(id);
                statusBuffers.delete(id);
                count++;
            }
        }
        return count;
    },

    getSupabaseClient: () => supabase
};