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:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user