Files
openhuman/src/store/threadSlice.ts
T
694dcfe3cc Feat/openclaw work - #131 (#151)
* feat: add initial project structure and documentation

- Introduced the GNU General Public License (GPL) v3 in LICENSE file.
- Added MCP configuration in .claude/mcp.json for server integration.
- Created architecture documentation in docs/ARCHITECTURE.md outlining the platform's design and components.
- Defined MVP specifications in docs/MVP.md for the Telegram-based Agent Assistant.
- Established API reference for team management in docs/teams-api-reference.md.
- Set up basic HTML structure in public/index.html and added logo image in public/logo.png.

* feat: add initial project documentation and HTML structure

- Introduced CODE_OF_CONDUCT.md to establish community guidelines and standards for behavior.
- Created CONTRIBUTING.md to outline contribution process, development setup, and project conventions.
- Added SECURITY.md to define the security policy, supported versions, and reporting procedures for vulnerabilities.
- Established basic HTML structure in index.html for the application interface.

* chore: remove hello-python skill files

- Deleted skill.json and skill.py files for the Hello Python example runtime skill, as they are no longer needed in the project.

* feat: port tinyhuman agent runtime from ZeroClaw into Tauri backend

Port daemon supervisor, health registry, security (policy, secrets, audit,
pairing), agent traits, and config modules from ZeroClaw (MIT) into a new
tinyhuman/ module under src-tauri/src/. The daemon auto-starts on desktop
and shuts down gracefully on app exit via CancellationToken.

- health: global HealthRegistry with component tracking and JSON snapshots
- security/policy: SecurityPolicy with command validation, risk levels, rate limiting
- security/secrets: ChaCha20-Poly1305 SecretStore with legacy XOR migration
- security/audit: AuditLogger with JSON-line events and log rotation
- security/pairing: PairingGuard with brute-force protection and SHA-256 hashing
- security/traits: Sandbox trait + NoopSandbox
- config: minimal DaemonConfig with autonomy, reliability, secrets, audit sub-configs
- daemon: supervisor with health state writer emitting Tauri events
- agent/traits: Provider, Tool, Memory, Observer, RuntimeAdapter traits + Noop impls
- commands/tinyhuman: Tauri commands for health, security policy, encrypt/decrypt
- 185 inline unit tests across all modules
- README updated with custom inference/tunneling/memory positioning

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: update README to reflect AlphaHuman Mk1 branding and enhanced description

- Changed project title to "AlphaHuman Mk1" for clarity.
- Revised project description to emphasize user-friendly AI capabilities and the use of the Neocortex Mk1 model.
- Removed outdated sections on custom inference, tunneling, and memory, streamlining the content for better readability.

* update readme

* Port zeroclaw runtime into tinyhuman

* Replace CLI mentions with UI language

* Split gateway module into smaller units

* Split channels and config schema modules

* Fix tinyhuman build, tests, and tunnel integration

* feat(tinyhuman): add missing modules and ui-friendly services

* refactor: rename tinyhuman to alphahuman

* chore: remove bottom text from Welcome component

* feat(settings): add tauri command console

* feat(daemon): enhance daemon mode handling and integrate rustls with ring feature

* feat(settings): implement comprehensive configuration management in TauriCommandsPanel

* refactor(TauriCommandsPanel): streamline error handling and enhance async function usage

* feat(settings): add skill management functionality to TauriCommandsPanel

* style(TauriCommandsPanel): update input styles for improved readability and user experience

* feat(settings): add Skills and Agent Chat panels with navigation and integration management

* feat(settings): implement browser access management in SkillsPanel and enhance AgentChatPanel with local storage functionality

* Implement inference API integration in Conversations component

- Added inference API service to handle chat completions and model management.
- Updated Conversations component to fetch available models on mount and allow model selection.
- Enhanced message sending functionality to utilize the inference API for generating responses.
- Introduced state management for loading models and handling errors during API calls.
- Refactored optimistic message handling and send status management for improved user experience.

---------

Co-authored-by: Steven Enamakel <enamakel@vezures.xyz>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Steven Enamakel <31011319+senamakel@users.noreply.github.com>
2026-02-26 19:58:21 +04:00

366 lines
11 KiB
TypeScript

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { threadApi } from '../services/api/threadApi';
import type { Thread, ThreadMessage } from '../types/thread';
interface ThreadState {
threads: Thread[];
isLoading: boolean;
error: string | null;
selectedThreadId: string | null;
messages: ThreadMessage[];
isLoadingMessages: boolean;
messagesError: string | null;
createStatus: 'idle' | 'loading' | 'success' | 'error';
deleteStatus: 'idle' | 'loading' | 'success' | 'error';
deleteError: string | null;
purgeStatus: 'idle' | 'loading' | 'success' | 'error';
sendStatus: 'idle' | 'loading' | 'success' | 'error';
sendError: string | null;
panelWidth: number;
/** threadId -> timestamp when user last viewed that thread (for unread indicators) */
lastViewedAt: Record<string, number>;
/** Suggested starter questions for empty threads (from GET /chat/autocomplete) */
suggestedQuestions: Array<{ text: string; confidence: number }>;
isLoadingSuggestions: boolean;
suggestError: string | null;
}
const initialState: ThreadState = {
threads: [],
isLoading: false,
error: null,
selectedThreadId: null,
messages: [],
isLoadingMessages: false,
messagesError: null,
createStatus: 'idle',
deleteStatus: 'idle',
deleteError: null,
purgeStatus: 'idle',
sendStatus: 'idle',
sendError: null,
panelWidth: 320,
lastViewedAt: {},
suggestedQuestions: [],
isLoadingSuggestions: false,
suggestError: null,
};
export const fetchThreads = createAsyncThunk(
'thread/fetchThreads',
async (_, { rejectWithValue }) => {
try {
const data = await threadApi.getThreads();
return data.threads;
} catch (error) {
const msg =
error && typeof error === 'object' && 'error' in error
? String(error.error)
: 'Failed to fetch threads';
return rejectWithValue(msg);
}
}
);
export const fetchThreadMessages = createAsyncThunk(
'thread/fetchThreadMessages',
async (threadId: string, { rejectWithValue }) => {
try {
const data = await threadApi.getThreadMessages(threadId);
return data.messages;
} catch (error) {
const msg =
error && typeof error === 'object' && 'error' in error
? String(error.error)
: 'Failed to fetch messages';
return rejectWithValue(msg);
}
}
);
export const createThread = createAsyncThunk(
'thread/createThread',
async (chatId: number | undefined, { dispatch, rejectWithValue }) => {
try {
const data = await threadApi.createThread(chatId);
dispatch(fetchThreads());
return data;
} catch (error) {
const msg =
error && typeof error === 'object' && 'error' in error
? String(error.error)
: 'Failed to create thread';
return rejectWithValue(msg);
}
}
);
export const deleteThread = createAsyncThunk(
'thread/deleteThread',
async (threadId: string, { dispatch, getState, rejectWithValue }) => {
try {
await threadApi.deleteThread(threadId);
const state = (getState() as { thread: ThreadState }).thread;
if (state.selectedThreadId === threadId) {
dispatch(clearSelectedThread());
}
return threadId;
} catch (error) {
const msg =
error && typeof error === 'object' && 'error' in error
? String(error.error)
: 'Failed to delete thread';
return rejectWithValue(msg);
}
}
);
export const purgeThreads = createAsyncThunk(
'thread/purgeThreads',
async (_, { dispatch, rejectWithValue }) => {
try {
const data = await threadApi.purge({
messages: false,
agentThreads: true,
deleteEverything: true,
});
dispatch(fetchThreads());
return data;
} catch (error) {
const msg =
error && typeof error === 'object' && 'error' in error
? String(error.error)
: 'Failed to purge threads';
return rejectWithValue(msg);
}
}
);
export const sendMessage = createAsyncThunk(
'thread/sendMessage',
async (
{ threadId, message }: { threadId: string; message: string },
{ dispatch, rejectWithValue }
) => {
try {
const data = await threadApi.sendMessage(message, threadId);
// Re-fetch messages to get the stored user message + agent response
dispatch(fetchThreadMessages(threadId));
// Re-fetch threads to update lastMessageAt / messageCount in the list
dispatch(fetchThreads());
return data;
} catch (error) {
const msg =
error && typeof error === 'object' && 'error' in error
? String(error.error)
: 'Failed to send message';
return rejectWithValue(msg);
}
}
);
export const fetchSuggestedQuestions = createAsyncThunk(
'thread/fetchSuggestedQuestions',
async (conversationId: string | undefined, { rejectWithValue }) => {
try {
const data = await threadApi.getSuggestQuestions(conversationId);
return data.suggestions;
} catch (error) {
const msg =
error && typeof error === 'object' && 'error' in error
? String(error.error)
: 'Failed to load suggestions';
return rejectWithValue(msg);
}
}
);
const threadSlice = createSlice({
name: 'thread',
initialState,
reducers: {
setSelectedThread: (state, action: { payload: string }) => {
state.selectedThreadId = action.payload;
state.messages = [];
state.messagesError = null;
state.suggestedQuestions = [];
state.suggestError = null;
},
clearSelectedThread: state => {
state.selectedThreadId = null;
state.messages = [];
state.messagesError = null;
state.suggestedQuestions = [];
state.suggestError = null;
},
clearSuggestedQuestions: state => {
state.suggestedQuestions = [];
state.suggestError = null;
},
clearCreateStatus: state => {
state.createStatus = 'idle';
},
clearDeleteStatus: state => {
state.deleteStatus = 'idle';
state.deleteError = null;
},
clearPurgeStatus: state => {
state.purgeStatus = 'idle';
},
addOptimisticMessage: (state, action: { payload: { content: string } }) => {
state.messages.push({
id: `optimistic-${Date.now()}`,
content: action.payload.content,
type: 'text',
extraMetadata: {},
sender: 'user',
createdAt: new Date().toISOString(),
});
},
addInferenceResponse: (state, action: { payload: { content: string } }) => {
state.messages.push({
id: `inference-${Date.now()}`,
content: action.payload.content,
type: 'text',
extraMetadata: {},
sender: 'agent',
createdAt: new Date().toISOString(),
});
},
removeOptimisticMessages: state => {
state.messages = state.messages.filter(m => !m.id.startsWith('optimistic-'));
},
clearSendError: state => {
state.sendError = null;
},
setPanelWidth: (state, action: { payload: number }) => {
state.panelWidth = action.payload;
},
setLastViewed: (state, action: { payload: string }) => {
const ts = Date.now();
state.lastViewedAt[action.payload] = ts;
},
},
extraReducers: builder => {
builder
// fetchThreads — only show skeleton on initial load (stale-while-revalidate)
.addCase(fetchThreads.pending, state => {
if (state.threads.length === 0) {
state.isLoading = true;
}
state.error = null;
})
.addCase(fetchThreads.fulfilled, (state, action) => {
state.isLoading = false;
state.threads = action.payload;
})
.addCase(fetchThreads.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
})
// fetchThreadMessages
.addCase(fetchThreadMessages.pending, state => {
state.isLoadingMessages = true;
state.messagesError = null;
})
.addCase(fetchThreadMessages.fulfilled, (state, action) => {
state.isLoadingMessages = false;
state.messages = action.payload;
// Hide suggestions once thread has messages
if (action.payload.length > 0) {
state.suggestedQuestions = [];
state.suggestError = null;
}
})
.addCase(fetchThreadMessages.rejected, (state, action) => {
state.isLoadingMessages = false;
state.messagesError = action.payload as string;
})
// createThread
.addCase(createThread.pending, state => {
state.createStatus = 'loading';
})
.addCase(createThread.fulfilled, (state, action) => {
state.createStatus = 'success';
state.selectedThreadId = action.payload.id;
state.messages = [];
state.messagesError = null;
})
.addCase(createThread.rejected, state => {
state.createStatus = 'error';
})
// deleteThread
.addCase(deleteThread.pending, state => {
state.deleteStatus = 'loading';
state.deleteError = null;
})
.addCase(deleteThread.fulfilled, (state, action) => {
state.deleteStatus = 'success';
state.threads = state.threads.filter(t => t.id !== action.payload);
})
.addCase(deleteThread.rejected, (state, action) => {
state.deleteStatus = 'error';
state.deleteError = action.payload as string;
})
// purgeThreads
.addCase(purgeThreads.pending, state => {
state.purgeStatus = 'loading';
})
.addCase(purgeThreads.fulfilled, state => {
state.purgeStatus = 'success';
state.selectedThreadId = null;
state.messages = [];
})
.addCase(purgeThreads.rejected, state => {
state.purgeStatus = 'error';
})
// sendMessage
.addCase(sendMessage.pending, state => {
state.sendStatus = 'loading';
state.sendError = null;
})
.addCase(sendMessage.fulfilled, state => {
state.sendStatus = 'success';
state.suggestedQuestions = [];
state.suggestError = null;
})
.addCase(sendMessage.rejected, (state, action) => {
state.sendStatus = 'error';
state.sendError = action.payload as string;
// Remove optimistic messages so the user doesn't see phantom messages
state.messages = state.messages.filter(m => !m.id.startsWith('optimistic-'));
})
// fetchSuggestedQuestions
.addCase(fetchSuggestedQuestions.pending, state => {
state.isLoadingSuggestions = true;
state.suggestError = null;
})
.addCase(fetchSuggestedQuestions.fulfilled, (state, action) => {
state.isLoadingSuggestions = false;
state.suggestedQuestions = action.payload;
})
.addCase(fetchSuggestedQuestions.rejected, (state, action) => {
state.isLoadingSuggestions = false;
state.suggestError = action.payload as string;
state.suggestedQuestions = [];
});
},
});
export const {
setSelectedThread,
clearSelectedThread,
clearCreateStatus,
clearDeleteStatus,
clearPurgeStatus,
addOptimisticMessage,
addInferenceResponse,
removeOptimisticMessages,
clearSendError,
clearSuggestedQuestions,
setPanelWidth,
setLastViewed,
} = threadSlice.actions;
export default threadSlice.reducer;