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>
This commit is contained in:
co-authored by
Steven Enamakel
Claude Opus 4.6
Steven Enamakel
parent
ca11b001c8
commit
694dcfe3cc
File diff suppressed because it is too large
Load Diff
+96
-14
@@ -9,21 +9,22 @@ import {
|
||||
import Markdown from 'react-markdown';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { inferenceApi, type ModelInfo } from '../services/api/inferenceApi';
|
||||
import { useAppDispatch, useAppSelector } from '../store/hooks';
|
||||
import {
|
||||
addInferenceResponse,
|
||||
addOptimisticMessage,
|
||||
clearCreateStatus,
|
||||
clearDeleteStatus,
|
||||
clearPurgeStatus,
|
||||
clearSelectedThread,
|
||||
clearSendError,
|
||||
createThread,
|
||||
deleteThread,
|
||||
fetchSuggestedQuestions,
|
||||
fetchThreadMessages,
|
||||
fetchThreads,
|
||||
purgeThreads,
|
||||
sendMessage,
|
||||
removeOptimisticMessages,
|
||||
setLastViewed,
|
||||
setPanelWidth,
|
||||
setSelectedThread,
|
||||
@@ -60,8 +61,6 @@ const Conversations = () => {
|
||||
createStatus,
|
||||
deleteStatus,
|
||||
purgeStatus,
|
||||
sendStatus,
|
||||
sendError,
|
||||
panelWidth,
|
||||
lastViewedAt,
|
||||
suggestedQuestions,
|
||||
@@ -73,6 +72,13 @@ const Conversations = () => {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
|
||||
|
||||
// Inference model state
|
||||
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
|
||||
const [selectedModel, setSelectedModel] = useState('neocortex-mk1');
|
||||
const [isLoadingModels, setIsLoadingModels] = useState(false);
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const [sendError, setSendError] = useState<string | null>(null);
|
||||
const isDragging = useRef(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const lastPanelWidthRef = useRef(panelWidth);
|
||||
@@ -134,6 +140,23 @@ const Conversations = () => {
|
||||
[panelWidth, dispatch]
|
||||
);
|
||||
|
||||
// Fetch available inference models on mount
|
||||
useEffect(() => {
|
||||
setIsLoadingModels(true);
|
||||
inferenceApi
|
||||
.listModels()
|
||||
.then(data => {
|
||||
if (data.data.length > 0) {
|
||||
setAvailableModels(data.data);
|
||||
setSelectedModel(data.data[0].id);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Keep default model on failure
|
||||
})
|
||||
.finally(() => setIsLoadingModels(false));
|
||||
}, []);
|
||||
|
||||
// Fetch threads on mount
|
||||
useEffect(() => {
|
||||
dispatch(fetchThreads());
|
||||
@@ -196,9 +219,9 @@ const Conversations = () => {
|
||||
// Clear send error when user starts typing again
|
||||
useEffect(() => {
|
||||
if (sendError && inputValue.length > 0) {
|
||||
dispatch(clearSendError());
|
||||
setSendError(null);
|
||||
}
|
||||
}, [inputValue, sendError, dispatch]);
|
||||
}, [inputValue, sendError]);
|
||||
|
||||
const handleSelectThread = (threadId: string) => {
|
||||
if (threadId === selectedThreadId) return;
|
||||
@@ -228,12 +251,44 @@ const Conversations = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendMessage = (text?: string) => {
|
||||
const handleSendMessage = async (text?: string) => {
|
||||
const trimmed = text ?? inputValue.trim();
|
||||
if (!trimmed || !selectedThreadId || sendStatus === 'loading') return;
|
||||
if (!trimmed || !selectedThreadId || isSending) return;
|
||||
|
||||
// Snapshot history before the optimistic update (exclude stale optimistic msgs)
|
||||
const historySnapshot = messages.filter(m => !m.id.startsWith('optimistic-'));
|
||||
|
||||
dispatch(addOptimisticMessage({ content: trimmed }));
|
||||
setInputValue('');
|
||||
dispatch(sendMessage({ threadId: selectedThreadId, message: trimmed }));
|
||||
setSendError(null);
|
||||
setIsSending(true);
|
||||
|
||||
try {
|
||||
const chatMessages = [
|
||||
...historySnapshot.map(m => ({
|
||||
role: (m.sender === 'user' ? 'user' : 'assistant') as 'user' | 'assistant',
|
||||
content: m.content,
|
||||
})),
|
||||
{ role: 'user' as const, content: trimmed },
|
||||
];
|
||||
|
||||
const response = await inferenceApi.createChatCompletion({
|
||||
model: selectedModel,
|
||||
messages: chatMessages,
|
||||
});
|
||||
|
||||
const content = response.choices[0]?.message?.content ?? '';
|
||||
dispatch(addInferenceResponse({ content }));
|
||||
} catch (err) {
|
||||
dispatch(removeOptimisticMessages());
|
||||
const msg =
|
||||
err && typeof err === 'object' && 'error' in err
|
||||
? String((err as { error: unknown }).error)
|
||||
: 'Failed to get response';
|
||||
setSendError(msg);
|
||||
} finally {
|
||||
setIsSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
@@ -683,7 +738,7 @@ const Conversations = () => {
|
||||
</div>
|
||||
))}
|
||||
{/* Typing indicator (#14) */}
|
||||
{sendStatus === 'loading' && (
|
||||
{isSending && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-white/5 rounded-2xl rounded-bl-md px-4 py-3">
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -712,7 +767,7 @@ const Conversations = () => {
|
||||
key={i}
|
||||
type="button"
|
||||
onClick={() => handleSendMessage(s.text)}
|
||||
disabled={sendStatus === 'loading'}
|
||||
disabled={isSending}
|
||||
className="flex-shrink-0 px-3 py-1.5 rounded-lg text-[12px] whitespace-nowrap bg-white/5 text-stone-400 hover:bg-white/10 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
{s.text}
|
||||
</button>
|
||||
@@ -723,11 +778,38 @@ const Conversations = () => {
|
||||
|
||||
{/* Message Input */}
|
||||
<div className="flex-shrink-0 border-t border-white/10 px-4 py-3">
|
||||
{/* Model selector */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{isLoadingModels ? (
|
||||
<span className="text-xs text-stone-600">Loading models…</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-xs text-stone-500">Model</span>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={e => setSelectedModel(e.target.value)}
|
||||
disabled={isSending}
|
||||
className="bg-white/5 border border-white/10 rounded-lg px-2 py-1 text-xs text-stone-300 focus:outline-none focus:ring-1 focus:ring-primary-500/50 disabled:opacity-50 cursor-pointer">
|
||||
{availableModels.length > 0 ? (
|
||||
availableModels.map(m => (
|
||||
<option key={m.id} value={m.id} className="bg-stone-900">
|
||||
{m.id}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option value={selectedModel} className="bg-stone-900">
|
||||
{selectedModel}
|
||||
</option>
|
||||
)}
|
||||
</select>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{sendError && (
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-xs text-coral-500">{sendError}</p>
|
||||
<button
|
||||
onClick={() => dispatch(clearSendError())}
|
||||
onClick={() => setSendError(null)}
|
||||
className="text-xs text-stone-500 hover:text-stone-300 transition-colors ml-2 flex-shrink-0">
|
||||
Dismiss
|
||||
</button>
|
||||
@@ -744,9 +826,9 @@ const Conversations = () => {
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleSendMessage()}
|
||||
disabled={!inputValue.trim() || sendStatus === 'loading'}
|
||||
disabled={!inputValue.trim() || isSending}
|
||||
className="p-2.5 rounded-xl bg-primary-600 hover:bg-primary-500 disabled:opacity-40 disabled:cursor-not-allowed transition-colors flex-shrink-0">
|
||||
{sendStatus === 'loading' ? (
|
||||
{isSending ? (
|
||||
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle
|
||||
className="opacity-25"
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { apiClient } from '../apiClient';
|
||||
|
||||
// ── Request types ────────────────────────────────────────────────────────────
|
||||
|
||||
export type ChatRole = 'system' | 'user' | 'assistant';
|
||||
|
||||
export interface ChatMessage {
|
||||
role: ChatRole;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ChatCompletionRequest {
|
||||
model: string;
|
||||
messages: ChatMessage[];
|
||||
stream?: boolean;
|
||||
temperature?: number;
|
||||
max_tokens?: number;
|
||||
}
|
||||
|
||||
export interface TextCompletionRequest {
|
||||
model: string;
|
||||
prompt: string;
|
||||
stream?: boolean;
|
||||
temperature?: number;
|
||||
max_tokens?: number;
|
||||
}
|
||||
|
||||
// ── Response types (OpenAI-compatible) ───────────────────────────────────────
|
||||
|
||||
export interface ModelInfo {
|
||||
id: string;
|
||||
object: string;
|
||||
created: number;
|
||||
owned_by: string;
|
||||
}
|
||||
|
||||
export interface ModelsListResponse {
|
||||
object: string;
|
||||
data: ModelInfo[];
|
||||
}
|
||||
|
||||
export interface ChatCompletionChoice {
|
||||
index: number;
|
||||
message: ChatMessage;
|
||||
finish_reason: string | null;
|
||||
}
|
||||
|
||||
export interface TextCompletionChoice {
|
||||
index: number;
|
||||
text: string;
|
||||
finish_reason: string | null;
|
||||
}
|
||||
|
||||
export interface CompletionUsage {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
}
|
||||
|
||||
export interface ChatCompletionResponse {
|
||||
id: string;
|
||||
object: string;
|
||||
created: number;
|
||||
model: string;
|
||||
choices: ChatCompletionChoice[];
|
||||
usage: CompletionUsage;
|
||||
}
|
||||
|
||||
export interface TextCompletionResponse {
|
||||
id: string;
|
||||
object: string;
|
||||
created: number;
|
||||
model: string;
|
||||
choices: TextCompletionChoice[];
|
||||
usage: CompletionUsage;
|
||||
}
|
||||
|
||||
// ── API ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const inferenceApi = {
|
||||
/** GET /openai/v1/models — list available models */
|
||||
listModels: async (): Promise<ModelsListResponse> => {
|
||||
return apiClient.get<ModelsListResponse>('/openai/v1/models');
|
||||
},
|
||||
|
||||
/** POST /openai/v1/chat/completions — create a chat completion */
|
||||
createChatCompletion: async (
|
||||
body: ChatCompletionRequest
|
||||
): Promise<ChatCompletionResponse> => {
|
||||
return apiClient.post<ChatCompletionResponse>('/openai/v1/chat/completions', body);
|
||||
},
|
||||
|
||||
/** POST /openai/v1/completions — create a text completion */
|
||||
createCompletion: async (
|
||||
body: TextCompletionRequest
|
||||
): Promise<TextCompletionResponse> => {
|
||||
return apiClient.post<TextCompletionResponse>('/openai/v1/completions', body);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { ApiResponse } from '../../types/api';
|
||||
import { apiClient } from '../apiClient';
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface Tunnel {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface TunnelBandwidthUsage {
|
||||
usedBytes: number;
|
||||
limitBytes: number;
|
||||
cycleStartDate: string;
|
||||
cycleEndDate: string;
|
||||
}
|
||||
|
||||
export interface CreateTunnelRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface UpdateTunnelRequest {
|
||||
name?: string;
|
||||
description?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
// ── API ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const tunnelsApi = {
|
||||
/** POST /tunnels — create a new webhook tunnel */
|
||||
createTunnel: async (body: CreateTunnelRequest): Promise<Tunnel> => {
|
||||
const response = await apiClient.post<ApiResponse<Tunnel>>('/tunnels', body);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/** GET /tunnels — list user's webhook tunnels */
|
||||
getTunnels: async (): Promise<Tunnel[]> => {
|
||||
const response = await apiClient.get<ApiResponse<Tunnel[]>>('/tunnels');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/** GET /tunnels/bandwidth — get bandwidth usage for current billing cycle */
|
||||
getBandwidthUsage: async (): Promise<TunnelBandwidthUsage> => {
|
||||
const response = await apiClient.get<ApiResponse<TunnelBandwidthUsage>>('/tunnels/bandwidth');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/** GET /tunnels/:id — get a specific webhook tunnel */
|
||||
getTunnel: async (id: string): Promise<Tunnel> => {
|
||||
const response = await apiClient.get<ApiResponse<Tunnel>>(`/tunnels/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/** PATCH /tunnels/:id — update a webhook tunnel */
|
||||
updateTunnel: async (id: string, body: UpdateTunnelRequest): Promise<Tunnel> => {
|
||||
const response = await apiClient.patch<ApiResponse<Tunnel>>(`/tunnels/${id}`, body);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/** DELETE /tunnels/:id — delete a webhook tunnel */
|
||||
deleteTunnel: async (id: string): Promise<void> => {
|
||||
await apiClient.delete<ApiResponse<unknown>>(`/tunnels/${id}`);
|
||||
},
|
||||
};
|
||||
@@ -218,6 +218,16 @@ const threadSlice = createSlice({
|
||||
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-'));
|
||||
},
|
||||
@@ -345,6 +355,7 @@ export const {
|
||||
clearDeleteStatus,
|
||||
clearPurgeStatus,
|
||||
addOptimisticMessage,
|
||||
addInferenceResponse,
|
||||
removeOptimisticMessages,
|
||||
clearSendError,
|
||||
clearSuggestedQuestions,
|
||||
|
||||
Reference in New Issue
Block a user