首页布局优化前
This commit is contained in:
@@ -82,6 +82,7 @@ export function inferPreference(feedback: string, sentiment: FeedbackSentiment):
|
||||
export class ActiveLearningEngine {
|
||||
private events: LearningEvent[] = [];
|
||||
private patterns: LearningPattern[] = [];
|
||||
// Reserved for future learning suggestions feature
|
||||
private suggestions: LearningSuggestion[] = [];
|
||||
private initialized: boolean = false;
|
||||
|
||||
@@ -89,6 +90,16 @@ export class ActiveLearningEngine {
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/** Get current suggestions (reserved for future use) */
|
||||
getSuggestions(): LearningSuggestion[] {
|
||||
return this.suggestions;
|
||||
}
|
||||
|
||||
/** Check if engine is initialized */
|
||||
isInitialized(): boolean {
|
||||
return this.initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录学习事件
|
||||
*/
|
||||
|
||||
@@ -190,7 +190,7 @@ export class AutonomyManager {
|
||||
|
||||
// High-risk actions ALWAYS require approval
|
||||
const isHighRisk = riskLevel === 'high';
|
||||
const isSelfModification = action === 'identity_update' || action === 'selfModification';
|
||||
const isSelfModification = action === 'identity_update';
|
||||
const isDeletion = action === 'memory_delete';
|
||||
|
||||
let allowed = false;
|
||||
|
||||
@@ -618,7 +618,26 @@ export class GatewayClient {
|
||||
// === High-level API ===
|
||||
|
||||
// Default agent ID for OpenFang (will be set dynamically from /api/agents)
|
||||
private defaultAgentId: string = 'f77004c8-418f-4132-b7d4-7ecb9d66f44c';
|
||||
private defaultAgentId: string = '';
|
||||
|
||||
/** Try to fetch default agent ID from OpenFang /api/agents endpoint */
|
||||
async fetchDefaultAgentId(): Promise<string | null> {
|
||||
try {
|
||||
// Use /api/agents endpoint which returns array of agents
|
||||
const agents = await this.restGet<Array<{ id: string; name?: string; state?: string }>>('/api/agents');
|
||||
if (agents && agents.length > 0) {
|
||||
// Prefer agent with state "Running", otherwise use first agent
|
||||
const runningAgent = agents.find((a: { id: string; name?: string; state?: string }) => a.state === 'Running');
|
||||
const defaultAgent = runningAgent || agents[0];
|
||||
this.defaultAgentId = defaultAgent.id;
|
||||
this.log('info', `Fetched default agent from /api/agents: ${this.defaultAgentId} (${defaultAgent.name || 'unnamed'})`);
|
||||
return this.defaultAgentId;
|
||||
}
|
||||
} catch (err) {
|
||||
this.log('warn', `Failed to fetch default agent from /api/agents: ${err}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Set the default agent ID */
|
||||
setDefaultAgentId(agentId: string): void {
|
||||
@@ -642,7 +661,18 @@ export class GatewayClient {
|
||||
maxTokens?: number;
|
||||
}): Promise<{ runId: string; sessionId?: string; response?: string }> {
|
||||
// OpenFang uses /api/agents/{agentId}/message endpoint
|
||||
const agentId = opts?.agentId || this.defaultAgentId;
|
||||
let agentId = opts?.agentId || this.defaultAgentId;
|
||||
|
||||
// If no agent ID, try to fetch from OpenFang status
|
||||
if (!agentId) {
|
||||
await this.fetchDefaultAgentId();
|
||||
agentId = this.defaultAgentId;
|
||||
}
|
||||
|
||||
if (!agentId) {
|
||||
throw new Error('No agent available. Please ensure OpenFang has at least one agent.');
|
||||
}
|
||||
|
||||
const result = await this.restPost<{ response?: string; input_tokens?: number; output_tokens?: number }>(`/api/agents/${agentId}/message`, {
|
||||
message,
|
||||
session_id: opts?.sessionKey,
|
||||
@@ -670,10 +700,29 @@ export class GatewayClient {
|
||||
agentId?: string;
|
||||
}
|
||||
): Promise<{ runId: string }> {
|
||||
const agentId = opts?.agentId || this.defaultAgentId;
|
||||
let agentId = opts?.agentId || this.defaultAgentId;
|
||||
const runId = createIdempotencyKey();
|
||||
const sessionId = opts?.sessionKey || `session_${Date.now()}`;
|
||||
|
||||
// If no agent ID, try to fetch from OpenFang status (async, but we'll handle it in connectOpenFangStream)
|
||||
if (!agentId) {
|
||||
// Try to get default agent asynchronously
|
||||
this.fetchDefaultAgentId().then(() => {
|
||||
const resolvedAgentId = this.defaultAgentId;
|
||||
if (resolvedAgentId) {
|
||||
this.streamCallbacks.set(runId, callbacks);
|
||||
this.connectOpenFangStream(resolvedAgentId, runId, sessionId, message);
|
||||
} else {
|
||||
callbacks.onError('No agent available. Please ensure OpenFang has at least one agent.');
|
||||
callbacks.onComplete();
|
||||
}
|
||||
}).catch((err) => {
|
||||
callbacks.onError(`Failed to get agent: ${err}`);
|
||||
callbacks.onComplete();
|
||||
});
|
||||
return { runId };
|
||||
}
|
||||
|
||||
// Store callbacks for this run
|
||||
this.streamCallbacks.set(runId, callbacks);
|
||||
|
||||
@@ -1087,7 +1136,11 @@ export class GatewayClient {
|
||||
async getQuickConfig(): Promise<any> {
|
||||
try {
|
||||
// Use /api/config endpoint (OpenFang's actual config endpoint)
|
||||
const config = await this.restGet('/api/config');
|
||||
const config = await this.restGet<{
|
||||
data_dir?: string;
|
||||
home_dir?: string;
|
||||
default_model?: { model?: string; provider?: string };
|
||||
}>('/api/config');
|
||||
// Map OpenFang config to frontend expected format
|
||||
return {
|
||||
quickConfig: {
|
||||
@@ -1098,7 +1151,7 @@ export class GatewayClient {
|
||||
agentNickname: 'ZCLAW',
|
||||
scenarios: ['通用对话', '代码助手', '文档编写'],
|
||||
workspaceDir: config.data_dir || config.home_dir,
|
||||
gatewayUrl: this.baseUrl,
|
||||
gatewayUrl: this.getRestBaseUrl(),
|
||||
defaultModel: config.default_model?.model,
|
||||
defaultProvider: config.default_model?.provider,
|
||||
theme: 'dark',
|
||||
|
||||
@@ -90,10 +90,8 @@ const LLM_CONFIG_KEY = 'zclaw-llm-config';
|
||||
// === Mock Adapter (for testing) ===
|
||||
|
||||
class MockLLMAdapter implements LLMServiceAdapter {
|
||||
private config: LLMConfig;
|
||||
|
||||
constructor(config: LLMConfig) {
|
||||
this.config = config;
|
||||
constructor(_config: LLMConfig) {
|
||||
// Config is stored for future use (e.g., custom mock behavior based on config)
|
||||
}
|
||||
|
||||
async complete(messages: LLMMessage[]): Promise<LLMResponse> {
|
||||
|
||||
@@ -9,10 +9,7 @@
|
||||
|
||||
import { useRef, useCallback, useMemo, useEffect, type CSSProperties, type ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import { VariableSizeList } from 'react-window';
|
||||
|
||||
// Type alias for convenience
|
||||
type List = VariableSizeList;
|
||||
import type { ListImperativeAPI } from 'react-window';
|
||||
|
||||
/**
|
||||
* Message item interface for virtualization
|
||||
@@ -52,8 +49,8 @@ const DEFAULT_HEIGHTS: Record<string, number> = {
|
||||
* Hook return type for virtualized message management
|
||||
*/
|
||||
export interface UseVirtualizedMessagesReturn {
|
||||
/** Reference to the VariableSizeList instance */
|
||||
listRef: React.RefObject<VariableSizeList | null>;
|
||||
/** Reference to the List instance */
|
||||
listRef: React.RefObject<ListImperativeAPI | null>;
|
||||
/** Get the current height for a message by id and role */
|
||||
getHeight: (id: string, role: string) => number;
|
||||
/** Update the measured height for a message */
|
||||
@@ -99,7 +96,7 @@ export function useVirtualizedMessages(
|
||||
messages: VirtualizedMessageItem[],
|
||||
defaultHeights: Record<string, number> = DEFAULT_HEIGHTS
|
||||
): UseVirtualizedMessagesReturn {
|
||||
const listRef = useRef<List>(null);
|
||||
const listRef = useRef<ListImperativeAPI>(null);
|
||||
const heightsRef = useRef<Map<string, number>>(new Map());
|
||||
const prevMessagesLengthRef = useRef<number>(0);
|
||||
|
||||
@@ -121,8 +118,7 @@ export function useVirtualizedMessages(
|
||||
const current = heightsRef.current.get(id);
|
||||
if (current !== height) {
|
||||
heightsRef.current.set(id, height);
|
||||
// Reset cache to force recalculation
|
||||
listRef.current?.resetAfterIndex(0);
|
||||
// Height updated - the list will use the new height on next render
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -141,7 +137,7 @@ export function useVirtualizedMessages(
|
||||
*/
|
||||
const scrollToBottom = useCallback((): void => {
|
||||
if (listRef.current && messages.length > 0) {
|
||||
listRef.current.scrollToItem(messages.length - 1, 'end');
|
||||
listRef.current.scrollToRow({ index: messages.length - 1, align: 'end' });
|
||||
}
|
||||
}, [messages.length]);
|
||||
|
||||
@@ -150,7 +146,7 @@ export function useVirtualizedMessages(
|
||||
*/
|
||||
const scrollToIndex = useCallback((index: number): void => {
|
||||
if (listRef.current && index >= 0 && index < messages.length) {
|
||||
listRef.current.scrollToItem(index, 'center');
|
||||
listRef.current.scrollToRow({ index, align: 'center' });
|
||||
}
|
||||
}, [messages.length]);
|
||||
|
||||
@@ -159,7 +155,6 @@ export function useVirtualizedMessages(
|
||||
*/
|
||||
const resetCache = useCallback((): void => {
|
||||
heightsRef.current.clear();
|
||||
listRef.current?.resetAfterIndex(0);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
|
||||
128
desktop/src/lib/use-onboarding.ts
Normal file
128
desktop/src/lib/use-onboarding.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* useOnboarding - Hook for detecting and managing first-time user onboarding
|
||||
*
|
||||
* Determines if user needs to go through the onboarding wizard.
|
||||
* Stores completion status in localStorage.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
const ONBOARDING_COMPLETED_KEY = 'zclaw-onboarding-completed';
|
||||
const USER_PROFILE_KEY = 'zclaw-user-profile';
|
||||
|
||||
export interface UserProfile {
|
||||
userName: string;
|
||||
userRole?: string;
|
||||
completedAt: string;
|
||||
}
|
||||
|
||||
export interface OnboardingState {
|
||||
isNeeded: boolean;
|
||||
isLoading: boolean;
|
||||
userProfile: UserProfile | null;
|
||||
markCompleted: (profile: Omit<UserProfile, 'completedAt'>) => void;
|
||||
resetOnboarding: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to manage first-time user onboarding
|
||||
*
|
||||
* Usage:
|
||||
* ```tsx
|
||||
* const { isNeeded, isLoading, markCompleted } = useOnboarding();
|
||||
*
|
||||
* if (isNeeded) {
|
||||
* return <OnboardingWizard onComplete={markCompleted} />;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useOnboarding(): OnboardingState {
|
||||
const [isNeeded, setIsNeeded] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
||||
|
||||
// Check onboarding status on mount
|
||||
useEffect(() => {
|
||||
try {
|
||||
const completed = localStorage.getItem(ONBOARDING_COMPLETED_KEY);
|
||||
const profileStr = localStorage.getItem(USER_PROFILE_KEY);
|
||||
|
||||
if (completed === 'true' && profileStr) {
|
||||
const profile = JSON.parse(profileStr) as UserProfile;
|
||||
setUserProfile(profile);
|
||||
setIsNeeded(false);
|
||||
} else {
|
||||
// No onboarding record - first time user
|
||||
setIsNeeded(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[useOnboarding] Failed to check onboarding status:', err);
|
||||
setIsNeeded(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Mark onboarding as completed
|
||||
const markCompleted = useCallback((profile: Omit<UserProfile, 'completedAt'>) => {
|
||||
const fullProfile: UserProfile = {
|
||||
...profile,
|
||||
completedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
try {
|
||||
localStorage.setItem(ONBOARDING_COMPLETED_KEY, 'true');
|
||||
localStorage.setItem(USER_PROFILE_KEY, JSON.stringify(fullProfile));
|
||||
setUserProfile(fullProfile);
|
||||
setIsNeeded(false);
|
||||
console.log('[useOnboarding] Onboarding completed for user:', profile.userName);
|
||||
} catch (err) {
|
||||
console.error('[useOnboarding] Failed to save onboarding status:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Reset onboarding (for testing or user request)
|
||||
const resetOnboarding = useCallback(() => {
|
||||
try {
|
||||
localStorage.removeItem(ONBOARDING_COMPLETED_KEY);
|
||||
localStorage.removeItem(USER_PROFILE_KEY);
|
||||
setUserProfile(null);
|
||||
setIsNeeded(true);
|
||||
console.log('[useOnboarding] Onboarding reset');
|
||||
} catch (err) {
|
||||
console.error('[useOnboarding] Failed to reset onboarding:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isNeeded,
|
||||
isLoading,
|
||||
userProfile,
|
||||
markCompleted,
|
||||
resetOnboarding,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored user profile without hook (for use outside React components)
|
||||
*/
|
||||
export function getStoredUserProfile(): UserProfile | null {
|
||||
try {
|
||||
const profileStr = localStorage.getItem(USER_PROFILE_KEY);
|
||||
if (profileStr) {
|
||||
return JSON.parse(profileStr) as UserProfile;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[useOnboarding] Failed to get user profile:', err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if onboarding is completed (for use outside React components)
|
||||
*/
|
||||
export function isOnboardingCompleted(): boolean {
|
||||
return localStorage.getItem(ONBOARDING_COMPLETED_KEY) === 'true';
|
||||
}
|
||||
|
||||
export default useOnboarding;
|
||||
@@ -123,7 +123,9 @@ export class VectorMemoryService {
|
||||
importance: Math.round((1 - result.score) * 10), // Invert score to importance
|
||||
createdAt: new Date().toISOString(),
|
||||
source: 'auto',
|
||||
tags: (result.metadata as Record<string, unknown>)?.tags ?? [],
|
||||
tags: Array.isArray((result.metadata as Record<string, unknown>)?.tags)
|
||||
? (result.metadata as Record<string, unknown>).tags as string[]
|
||||
: [],
|
||||
lastAccessedAt: new Date().toISOString(),
|
||||
accessCount: 0,
|
||||
};
|
||||
@@ -132,7 +134,9 @@ export class VectorMemoryService {
|
||||
memory,
|
||||
score: result.score,
|
||||
uri: result.uri,
|
||||
highlights: (result.metadata as Record<string, unknown>)?.highlights as string[] | undefined,
|
||||
highlights: Array.isArray((result.metadata as Record<string, unknown>)?.highlights)
|
||||
? (result.metadata as Record<string, unknown>).highlights as string[]
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface FindResult {
|
||||
level: ContextLevel;
|
||||
abstract?: string;
|
||||
overview?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface GrepOptions {
|
||||
|
||||
Reference in New Issue
Block a user