首页布局优化前

This commit is contained in:
iven
2026-03-17 23:26:16 +08:00
parent 74dbf42644
commit e262200f1e
89 changed files with 2266 additions and 2120 deletions

View File

@@ -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;
}
/**
* 记录学习事件
*/

View File

@@ -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;

View File

@@ -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',

View File

@@ -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> {

View File

@@ -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);
}, []);
/**

View 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;

View File

@@ -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,
});
}

View File

@@ -49,6 +49,7 @@ export interface FindResult {
level: ContextLevel;
abstract?: string;
overview?: string;
metadata?: Record<string, unknown>;
}
export interface GrepOptions {