mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 05:12:33 +00:00
* 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>
366 lines
11 KiB
TypeScript
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;
|