feat(chat): enhance chat functionality with beforeSend hooks and composer status components (#927)
This commit is contained in:
@@ -8,5 +8,9 @@ export type {
|
||||
SidebarExtension,
|
||||
RouteExtension,
|
||||
SettingsSectionExtension,
|
||||
ChatBeforeSendContext,
|
||||
ChatBeforeSendResult,
|
||||
ChatComposerStatusProps,
|
||||
ChatExtension,
|
||||
I18nResources,
|
||||
} from './types';
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import i18n from '@/i18n';
|
||||
import i18n from 'i18next';
|
||||
import type {
|
||||
RendererExtension,
|
||||
NavItemDef,
|
||||
RouteDef,
|
||||
SettingsSectionDef,
|
||||
SkillDetailMetaProps,
|
||||
ChatComposerStatusProps,
|
||||
ChatBeforeSendContext,
|
||||
ChatBeforeSendResult,
|
||||
} from './types';
|
||||
import type { ComponentType } from 'react';
|
||||
|
||||
@@ -62,6 +65,33 @@ class RendererExtensionRegistry {
|
||||
return this.extensions.flatMap((ext) => ext.skills?.detailMetaComponents ?? []);
|
||||
}
|
||||
|
||||
getChatComposerStatusComponents(): ComponentType<ChatComposerStatusProps>[] {
|
||||
return this.extensions.flatMap((ext) => ext.chat?.composerStatusComponents ?? []);
|
||||
}
|
||||
|
||||
hasChatBeforeSendHooks(): boolean {
|
||||
return this.extensions.some((ext) => (ext.chat?.beforeSend?.length ?? 0) > 0);
|
||||
}
|
||||
|
||||
async runChatBeforeSend(context: ChatBeforeSendContext): Promise<ChatBeforeSendResult> {
|
||||
for (const ext of this.extensions) {
|
||||
for (const hook of ext.chat?.beforeSend ?? []) {
|
||||
try {
|
||||
const result = await hook(context);
|
||||
if (!result.ok) {
|
||||
return result;
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async initializeAll(): Promise<void> {
|
||||
for (const ext of this.extensions) {
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ComponentType } from 'react';
|
||||
import type { Skill } from '../types/skill';
|
||||
import type { GatewayStatus } from '../types/gateway';
|
||||
|
||||
export interface NavItemDef {
|
||||
to: string;
|
||||
@@ -47,12 +48,36 @@ export interface SkillsExtension {
|
||||
detailMetaComponents?: ComponentType<SkillDetailMetaProps>[];
|
||||
}
|
||||
|
||||
export interface ChatBeforeSendContext {
|
||||
text: string;
|
||||
attachments?: unknown[];
|
||||
targetAgentId?: string | null;
|
||||
}
|
||||
|
||||
export interface ChatBeforeSendResult {
|
||||
ok: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ChatComposerStatusProps {
|
||||
gatewayStatus: GatewayStatus;
|
||||
}
|
||||
|
||||
export interface ChatExtension {
|
||||
id: string;
|
||||
composerStatusComponents?: ComponentType<ChatComposerStatusProps>[];
|
||||
beforeSend?: Array<(
|
||||
context: ChatBeforeSendContext,
|
||||
) => ChatBeforeSendResult | Promise<ChatBeforeSendResult>>;
|
||||
}
|
||||
|
||||
export interface RendererExtension {
|
||||
id: string;
|
||||
sidebar?: SidebarExtension;
|
||||
routes?: RouteExtension;
|
||||
settings?: SettingsSectionExtension;
|
||||
skills?: SkillsExtension;
|
||||
chat?: ChatExtension;
|
||||
i18nResources?: I18nResources;
|
||||
setup?(): void | Promise<void>;
|
||||
teardown?(): void;
|
||||
|
||||
@@ -18,6 +18,8 @@ import { useAgentsStore } from '@/stores/agents';
|
||||
import { useChatStore } from '@/stores/chat';
|
||||
import type { AgentSummary } from '@/types/agent';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { rendererExtensionRegistry } from '@/extensions/registry';
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -109,6 +111,7 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
|
||||
[agents, targetAgentId],
|
||||
);
|
||||
const showAgentPicker = mentionableAgents.length > 0;
|
||||
const chatComposerStatusComponents = rendererExtensionRegistry.getChatComposerStatusComponents();
|
||||
|
||||
// Auto-resize textarea
|
||||
useEffect(() => {
|
||||
@@ -288,13 +291,28 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
|
||||
const canSend = (input.trim() || attachments.length > 0) && allReady && !disabled && !sending;
|
||||
const canStop = sending && !disabled && !!onStop;
|
||||
|
||||
const handleSend = useCallback(() => {
|
||||
const handleSend = useCallback(async () => {
|
||||
if (!canSend) return;
|
||||
const readyAttachments = attachments.filter(a => a.status === 'ready');
|
||||
// Capture values before clearing — clear input immediately for snappy UX,
|
||||
// but keep attachments available for the async send
|
||||
const textToSend = input.trim();
|
||||
const attachmentsToSend = readyAttachments.length > 0 ? readyAttachments : undefined;
|
||||
|
||||
if (rendererExtensionRegistry.hasChatBeforeSendHooks()) {
|
||||
const guard = await rendererExtensionRegistry.runChatBeforeSend({
|
||||
text: textToSend,
|
||||
attachments: attachmentsToSend,
|
||||
targetAgentId,
|
||||
});
|
||||
if (!guard.ok) {
|
||||
if (guard.message) {
|
||||
toast.error(guard.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Capture values before clearing — clear input immediately for snappy UX,
|
||||
// but keep attachments available for the async send
|
||||
console.log(`[handleSend] text="${textToSend.substring(0, 50)}", attachments=${attachments.length}, ready=${readyAttachments.length}, sending=${!!attachmentsToSend}`);
|
||||
if (attachmentsToSend) {
|
||||
console.log('[handleSend] Attachment details:', attachmentsToSend.map(a => ({
|
||||
@@ -530,6 +548,9 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
|
||||
pid: gatewayStatus.pid ? `| pid: ${gatewayStatus.pid}` : '',
|
||||
})}
|
||||
</span>
|
||||
{chatComposerStatusComponents.map((Component, index) => (
|
||||
<Component key={`${index}`} gatewayStatus={gatewayStatus} />
|
||||
))}
|
||||
</div>
|
||||
{hasFailedAttachments && (
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user