Chore/build npm (#9)

Co-authored-by: DigHuang <114602213+DigHuang@users.noreply.github.com>
Co-authored-by: Felix <24791380+vcfgv@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Haze
2026-02-09 15:10:08 +08:00
committed by GitHub
Unverified
parent 0b7f1c700e
commit de445ae3d5
37 changed files with 7359 additions and 1586 deletions

View File

@@ -2,7 +2,7 @@
* Channels Page
* Manage messaging channel connections with configuration UI
*/
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import {
Plus,
Radio,
@@ -71,7 +71,7 @@ export function Channels() {
}, [fetchChannels]);
// Fetch configured channel types from config file
const fetchConfiguredTypes = async () => {
const fetchConfiguredTypes = useCallback(async () => {
try {
const result = await window.electron.ipcRenderer.invoke('channel:listConfigured') as {
success: boolean;
@@ -83,11 +83,12 @@ export function Channels() {
} catch {
// ignore
}
};
}, []);
useEffect(() => {
fetchConfiguredTypes();
}, []);
// eslint-disable-next-line react-hooks/set-state-in-effect
void fetchConfiguredTypes();
}, [fetchConfiguredTypes]);
// Get channel types to display
const displayedChannelTypes = showAllChannels ? getAllChannels() : getPrimaryChannels();

View File

@@ -4,7 +4,7 @@
* via gateway:rpc IPC. Session selector, thinking toggle, and refresh
* are in the toolbar; messages render with markdown + streaming.
*/
import { useEffect, useRef } from 'react';
import { useEffect, useRef, useState } from 'react';
import { AlertCircle, Bot, MessageSquare, Sparkles } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { useChatStore } from '@/stores/chat';
@@ -30,6 +30,7 @@ export function Chat() {
const clearError = useChatStore((s) => s.clearError);
const messagesEndRef = useRef<HTMLDivElement>(null);
const [streamingTimestamp, setStreamingTimestamp] = useState<number>(0);
// Load data when gateway is running
useEffect(() => {
@@ -44,6 +45,16 @@ export function Chat() {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, streamingMessage, sending]);
// Update timestamp when sending starts
useEffect(() => {
if (sending && streamingTimestamp === 0) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setStreamingTimestamp(Date.now() / 1000);
} else if (!sending && streamingTimestamp !== 0) {
setStreamingTimestamp(0);
}
}, [sending, streamingTimestamp]);
// Gateway not running
if (!isGatewayRunning) {
return (
@@ -88,7 +99,7 @@ export function Chat() {
message={{
role: 'assistant',
content: streamingMessage as unknown as string,
timestamp: Date.now() / 1000,
timestamp: streamingTimestamp,
}}
showThinking={showThinking}
isStreaming

View File

@@ -2,7 +2,7 @@
* Dashboard Page
* Main overview page showing system status and quick actions
*/
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import {
Activity,
MessageSquare,
@@ -27,6 +27,7 @@ export function Dashboard() {
const { skills, fetchSkills } = useSkillsStore();
const isGatewayRunning = gatewayStatus.state === 'running';
const [uptime, setUptime] = useState(0);
// Fetch data only when gateway is running
useEffect(() => {
@@ -40,10 +41,24 @@ export function Dashboard() {
const connectedChannels = Array.isArray(channels) ? channels.filter((c) => c.status === 'connected').length : 0;
const enabledSkills = Array.isArray(skills) ? skills.filter((s) => s.enabled).length : 0;
// Calculate uptime
const uptime = gatewayStatus.connectedAt
? Math.floor((Date.now() - gatewayStatus.connectedAt) / 1000)
: 0;
// Update uptime periodically
useEffect(() => {
const updateUptime = () => {
if (gatewayStatus.connectedAt) {
setUptime(Math.floor((Date.now() - gatewayStatus.connectedAt) / 1000));
} else {
setUptime(0);
}
};
// Update immediately
updateUptime();
// Update every second
const interval = setInterval(updateUptime, 1000);
return () => clearInterval(interval);
}, [gatewayStatus.connectedAt]);
return (
<div className="space-y-6">

View File

@@ -2,7 +2,7 @@
* Setup Wizard Page
* First-time setup experience for new users
*/
import { useState, useEffect, useCallback, useRef } from 'react';
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import {
@@ -119,13 +119,14 @@ const providers: Provider[] = [
export function Setup() {
const navigate = useNavigate();
const [currentStep, setCurrentStep] = useState(0);
const [canProceed, setCanProceed] = useState(true);
// Setup state
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
const [apiKey, setApiKey] = useState('');
// Installation state for the Installing step
const [installedSkills, setInstalledSkills] = useState<string[]>([]);
// Runtime check status
const [runtimeChecksPassed, setRuntimeChecksPassed] = useState(false);
const step = steps[currentStep];
const isFirstStep = currentStep === 0;
@@ -133,6 +134,26 @@ export function Setup() {
const markSetupComplete = useSettingsStore((state) => state.markSetupComplete);
// Derive canProceed based on current step - computed directly to avoid useEffect
const canProceed = useMemo(() => {
switch (currentStep) {
case STEP.WELCOME:
return true;
case STEP.RUNTIME:
return runtimeChecksPassed;
case STEP.PROVIDER:
return selectedProvider !== null && apiKey.length > 0;
case STEP.CHANNEL:
return true; // Always allow proceeding — channel step is optional
case STEP.INSTALLING:
return false; // Cannot manually proceed, auto-proceeds when done
case STEP.COMPLETE:
return true;
default:
return true;
}
}, [currentStep, selectedProvider, apiKey, runtimeChecksPassed]);
const handleNext = async () => {
if (isLastStep) {
// Complete setup
@@ -162,31 +183,6 @@ export function Setup() {
}, 1000);
}, []);
// Update canProceed based on current step
useEffect(() => {
switch (currentStep) {
case STEP.WELCOME:
setCanProceed(true);
break;
case STEP.RUNTIME:
// Will be managed by RuntimeContent
break;
case STEP.PROVIDER:
setCanProceed(selectedProvider !== null && apiKey.length > 0);
break;
case STEP.CHANNEL:
// Always allow proceeding — channel step is optional
setCanProceed(true);
break;
case STEP.INSTALLING:
setCanProceed(false); // Cannot manually proceed, auto-proceeds when done
break;
case STEP.COMPLETE:
setCanProceed(true);
break;
}
}, [currentStep, selectedProvider, apiKey]);
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800 text-white">
{/* Progress Indicator */}
@@ -240,7 +236,7 @@ export function Setup() {
{/* Step-specific content */}
<div className="rounded-xl bg-white/10 backdrop-blur p-8 mb-8">
{currentStep === STEP.WELCOME && <WelcomeContent />}
{currentStep === STEP.RUNTIME && <RuntimeContent onStatusChange={setCanProceed} />}
{currentStep === STEP.RUNTIME && <RuntimeContent onStatusChange={setRuntimeChecksPassed} />}
{currentStep === STEP.PROVIDER && (
<ProviderContent
providers={providers}
@@ -353,6 +349,9 @@ function RuntimeContent({ onStatusChange }: RuntimeContentProps) {
openclaw: { status: 'checking' as 'checking' | 'success' | 'error', message: '' },
gateway: { status: 'checking' as 'checking' | 'success' | 'error', message: '' },
});
const [showLogs, setShowLogs] = useState(false);
const [logContent, setLogContent] = useState('');
const [openclawDir, setOpenclawDir] = useState('');
const runChecks = useCallback(async () => {
// Reset checks
@@ -362,59 +361,53 @@ function RuntimeContent({ onStatusChange }: RuntimeContentProps) {
gateway: { status: 'checking', message: '' },
});
// Check Node.js
try {
// In Electron, we can assume Node.js is available
setChecks((prev) => ({
...prev,
nodejs: { status: 'success', message: 'Node.js is available' },
}));
} catch {
setChecks((prev) => ({
...prev,
nodejs: { status: 'error', message: 'Node.js not found' },
}));
}
// Check Node.js — always available in Electron
setChecks((prev) => ({
...prev,
nodejs: { status: 'success', message: 'Node.js is available (Electron built-in)' },
}));
// Check OpenClaw submodule status
// Check OpenClaw package status
try {
const openclawStatus = await window.electron.ipcRenderer.invoke('openclaw:status') as {
submoduleExists: boolean;
isInstalled: boolean;
packageExists: boolean;
isBuilt: boolean;
dir: string;
version?: string;
};
if (!openclawStatus.submoduleExists) {
setOpenclawDir(openclawStatus.dir);
if (!openclawStatus.packageExists) {
setChecks((prev) => ({
...prev,
openclaw: {
status: 'error',
message: 'OpenClaw submodule not found. Run: git submodule update --init'
message: `OpenClaw package not found at: ${openclawStatus.dir}`
},
}));
} else if (!openclawStatus.isInstalled) {
} else if (!openclawStatus.isBuilt) {
setChecks((prev) => ({
...prev,
openclaw: {
status: 'error',
message: 'Dependencies not installed. Run: cd openclaw && pnpm install'
message: 'OpenClaw package found but dist is missing'
},
}));
} else {
const modeLabel = openclawStatus.isBuilt ? 'production' : 'development';
const versionLabel = openclawStatus.version ? ` v${openclawStatus.version}` : '';
setChecks((prev) => ({
...prev,
openclaw: {
status: 'success',
message: `OpenClaw package ready (${modeLabel} mode)`
message: `OpenClaw package ready${versionLabel}`
},
}));
}
} catch (error) {
setChecks((prev) => ({
...prev,
openclaw: { status: 'error', message: `Failed to check: ${error}` },
openclaw: { status: 'error', message: `Check failed: ${error}` },
}));
}
@@ -433,7 +426,10 @@ function RuntimeContent({ onStatusChange }: RuntimeContentProps) {
} else {
setChecks((prev) => ({
...prev,
gateway: { status: 'error', message: 'Not running' },
gateway: {
status: 'error',
message: gatewayStatus.error || 'Not running'
},
}));
}
}, [gatewayStatus]);
@@ -473,6 +469,28 @@ function RuntimeContent({ onStatusChange }: RuntimeContentProps) {
await startGateway();
};
const handleShowLogs = async () => {
try {
const logs = await window.electron.ipcRenderer.invoke('log:readFile', 100) as string;
setLogContent(logs);
setShowLogs(true);
} catch {
setLogContent('(Failed to load logs)');
setShowLogs(true);
}
};
const handleOpenLogDir = async () => {
try {
const logDir = await window.electron.ipcRenderer.invoke('log:getDir') as string;
if (logDir) {
await window.electron.ipcRenderer.invoke('shell:showItemInFolder', logDir);
}
} catch {
// ignore
}
};
const renderStatus = (status: 'checking' | 'success' | 'error', message: string) => {
if (status === 'checking') {
return (
@@ -502,10 +520,15 @@ function RuntimeContent({ onStatusChange }: RuntimeContentProps) {
<div className="space-y-4">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold">Checking Environment</h2>
<Button variant="ghost" size="sm" onClick={runChecks}>
<RefreshCw className="h-4 w-4 mr-2" />
Re-check
</Button>
<div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={handleShowLogs}>
View Logs
</Button>
<Button variant="ghost" size="sm" onClick={runChecks}>
<RefreshCw className="h-4 w-4 mr-2" />
Re-check
</Button>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 rounded-lg bg-white/5">
@@ -513,7 +536,14 @@ function RuntimeContent({ onStatusChange }: RuntimeContentProps) {
{renderStatus(checks.nodejs.status, checks.nodejs.message)}
</div>
<div className="flex items-center justify-between p-3 rounded-lg bg-white/5">
<span>OpenClaw Package</span>
<div>
<span>OpenClaw Package</span>
{openclawDir && (
<p className="text-xs text-slate-500 mt-0.5 font-mono truncate max-w-[300px]">
{openclawDir}
</p>
)}
</div>
{renderStatus(checks.openclaw.status, checks.openclaw.message)}
</div>
<div className="flex items-center justify-between p-3 rounded-lg bg-white/5">
@@ -536,12 +566,33 @@ function RuntimeContent({ onStatusChange }: RuntimeContentProps) {
<div>
<p className="font-medium text-red-400">Environment issue detected</p>
<p className="text-sm text-slate-300 mt-1">
Please ensure Node.js is installed and OpenClaw is properly set up.
Please ensure OpenClaw is properly installed. Check the logs for details.
</p>
</div>
</div>
</div>
)}
{/* Log viewer panel */}
{showLogs && (
<div className="mt-4 p-4 rounded-lg bg-black/40 border border-slate-600">
<div className="flex items-center justify-between mb-2">
<p className="font-medium text-slate-200 text-sm">Application Logs</p>
<div className="flex gap-2">
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={handleOpenLogDir}>
<ExternalLink className="h-3 w-3 mr-1" />
Open Log Folder
</Button>
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => setShowLogs(false)}>
Close
</Button>
</div>
</div>
<pre className="text-xs text-slate-300 bg-black/50 p-3 rounded max-h-60 overflow-auto whitespace-pre-wrap font-mono">
{logContent || '(No logs available yet)'}
</pre>
</div>
)}
</div>
);
}

View File

@@ -548,7 +548,10 @@ export function Skills() {
setShowGatewayWarning(true);
}, 1500);
} else {
setShowGatewayWarning(false);
// Use setTimeout to avoid synchronous setState in effect
timer = setTimeout(() => {
setShowGatewayWarning(false);
}, 0);
}
return () => clearTimeout(timer);
}, [isGatewayRunning]);