feat(M6): Weitere Hosts — MiniApps in Fenstern + AI-Agenten-Ausgabe-Bloecke (#364)
- Windows-Host: openMiniAppWindow-Helper + MiniAppWindowContent (windowStore); Oeffnen-Buttons im Chat-Block (MiniAppBlock) und Dashboard-Widget - AI-Agenten-Host: Core-Tool send_miniapp (app/ai/miniapp_tools.py) — miniapp-Block in Agent-Chat (approval_request-Praezedenz), Permission fail-closed gegen aufrufenden User pro App; Registrierung im lifespan - agent_loop: tool_context + agent_name (Raum-Aufloesung) - Fix: MiniAppBlock nutzt useMiniapps (component-Feld) statt Legacy /comm/miniapps - Tests: M6 7/7 (TDD rot->gruen), Backend-Regression 57/57, Vitest 26/26 (4 neue Window-Tests), tsc clean, build OK
This commit is contained in:
@@ -1,42 +1,36 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { MessageBlock } from '@/store/commStore';
|
||||
import { AppWindow, Loader2 } from 'lucide-react';
|
||||
import { apiClient } from '@/api/client';
|
||||
import { AppWindow, ExternalLink, Loader2 } from 'lucide-react';
|
||||
import { useMiniapps, type MiniAppDef } from '@/api/miniapps';
|
||||
import { MiniAppWindowContent } from '@/components/dashboard/MiniAppWindowContent';
|
||||
import { openMiniAppWindow } from '@/components/dashboard/openMiniAppWindow';
|
||||
|
||||
interface MiniAppBlockProps {
|
||||
block: MessageBlock;
|
||||
}
|
||||
|
||||
interface MiniAppDef {
|
||||
app_id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
plugin_name: string;
|
||||
render_schema: Record<string, any>;
|
||||
}
|
||||
|
||||
const MiniAppBlock: React.FC<MiniAppBlockProps> = ({ block }) => {
|
||||
const { t } = useTranslation();
|
||||
const { app_id, config } = block.block_data;
|
||||
const [appDef, setAppDef] = useState<MiniAppDef | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
// M6: universal registry endpoint (carries component/permission fields;
|
||||
// the legacy /comm/miniapps list never did, hiding the window button)
|
||||
const { data: miniappsData, isLoading: loading } = useMiniapps();
|
||||
const appDef: MiniAppDef | null =
|
||||
miniappsData?.items.find((a) => a.app_id === app_id) ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!app_id) return;
|
||||
setLoading(true);
|
||||
apiClient.get('/comm/miniapps')
|
||||
.then((res) => {
|
||||
const apps: MiniAppDef[] = res.data || [];
|
||||
const found = apps.find((a) => a.app_id === app_id);
|
||||
setAppDef(found || null);
|
||||
})
|
||||
.catch(() => setAppDef(null))
|
||||
.finally(() => setLoading(false));
|
||||
}, [app_id]);
|
||||
const canOpenInWindow = Boolean(appDef?.component);
|
||||
const openInWindow = () =>
|
||||
openMiniAppWindow({
|
||||
appId: app_id || '',
|
||||
def: appDef ?? undefined,
|
||||
settings: (config as Record<string, unknown>) ?? {},
|
||||
component: MiniAppWindowContent,
|
||||
});
|
||||
|
||||
const appId: string = app_id || 'Unbekannt';
|
||||
const hasConfig = config && typeof config === 'object' && Object.keys(config).length > 0;
|
||||
const schema = appDef?.render_schema;
|
||||
const schema = appDef?.render_schema as { fields?: { name: string; label?: string }[] } | undefined;
|
||||
const hasSchema = schema && Object.keys(schema).length > 0;
|
||||
|
||||
if (loading) {
|
||||
@@ -60,6 +54,18 @@ const MiniAppBlock: React.FC<MiniAppBlockProps> = ({ block }) => {
|
||||
{appDef?.plugin_name && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-secondary-100 text-secondary-600">{appDef.plugin_name}</span>
|
||||
)}
|
||||
{canOpenInWindow && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openInWindow}
|
||||
className="p-1.5 rounded hover:bg-secondary-100 text-secondary-400 hover:text-primary-600"
|
||||
aria-label={t('dashboard.openInWindow')}
|
||||
title={t('dashboard.openInWindow')}
|
||||
data-testid={`miniapp-open-window-${app_id}`}
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasSchema && schema.fields && (
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import {
|
||||
ExternalLink,
|
||||
LayoutDashboard,
|
||||
Pencil,
|
||||
Check,
|
||||
@@ -56,6 +57,8 @@ import {
|
||||
} from '@/api/dashboards';
|
||||
import { useMiniapps, renderableDashboardApps, type MiniAppDef } from '@/api/miniapps';
|
||||
import { MiniAppHost } from './MiniAppHost';
|
||||
import { MiniAppWindowContent } from './MiniAppWindowContent';
|
||||
import { openMiniAppWindow } from './openMiniAppWindow';
|
||||
import { WidgetSettingsForm } from './WidgetSettingsForm';
|
||||
|
||||
let widgetIdCounter = 0;
|
||||
@@ -166,6 +169,22 @@ function SortableWidget({ widget, def, editMode, onRemove, onResize, onOpenSetti
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5 rotate-90" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
openMiniAppWindow({
|
||||
appId: widget.app_id,
|
||||
def,
|
||||
settings: widget.settings,
|
||||
component: MiniAppWindowContent,
|
||||
})
|
||||
}
|
||||
className="p-1.5 rounded hover:bg-secondary-100 text-secondary-500"
|
||||
aria-label={t('dashboard.openInWindow')}
|
||||
data-testid="dashboard-widget-open-window"
|
||||
>
|
||||
<ExternalLink className="w-3.5 h-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenSettings}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* MiniAppWindowContent — window host for a MiniApp (Phase M6).
|
||||
*
|
||||
* Rendered inside the window manager's floating windows: resolves the
|
||||
* MiniApp definition (name, settings_schema) and renders the shared
|
||||
* MiniAppHost as the window body.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMiniapps } from '@/api/miniapps';
|
||||
import { MiniAppHost } from '@/components/dashboard/MiniAppHost';
|
||||
|
||||
export interface MiniAppWindowContentProps {
|
||||
appId: string;
|
||||
settings?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function MiniAppWindowContent({ appId, settings }: MiniAppWindowContentProps) {
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading } = useMiniapps();
|
||||
const def = data?.items.find((a) => a.app_id === appId);
|
||||
|
||||
return (
|
||||
<div className="p-4 h-full overflow-auto" data-testid={`miniapp-window-${appId}`}>
|
||||
{isLoading ? (
|
||||
<p className="text-sm text-secondary-500">{t('common.loading', 'Lädt…')}</p>
|
||||
) : (
|
||||
<>
|
||||
{def?.description && (
|
||||
<p className="text-xs text-secondary-400 mb-3">{def.description}</p>
|
||||
)}
|
||||
<MiniAppHost appId={appId} settings={settings ?? {}} def={def} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* openMiniAppWindow — open a MiniApp in a floating window (Phase M6).
|
||||
*
|
||||
* Windows host: any inline MiniApp (chat block, dashboard widget) can be
|
||||
* popped into a draggable window via the existing window manager.
|
||||
*/
|
||||
|
||||
import type { ComponentType } from 'react';
|
||||
import { useWindowStore } from '@/store/windowStore';
|
||||
import type { MiniAppDef } from '@/api/miniapps';
|
||||
|
||||
export interface OpenMiniAppWindowOptions {
|
||||
appId: string;
|
||||
title?: string;
|
||||
settings?: Record<string, unknown>;
|
||||
def?: MiniAppDef;
|
||||
component: ComponentType<{ appId: string; settings?: Record<string, unknown> }>;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export function openMiniAppWindow(opts: OpenMiniAppWindowOptions): string {
|
||||
const store = useWindowStore.getState();
|
||||
const title = opts.title ?? opts.def?.name ?? opts.appId;
|
||||
const id = store.openWindow({
|
||||
title,
|
||||
type: `miniapp-${opts.appId}`,
|
||||
component: opts.component,
|
||||
componentProps: {
|
||||
appId: opts.appId,
|
||||
settings: opts.settings ?? {},
|
||||
},
|
||||
});
|
||||
// openWindow applies the default size; MiniApps get a compact default
|
||||
store.updateWindowSize(id, { width: opts.width ?? 520, height: opts.height ?? 480 });
|
||||
return id;
|
||||
}
|
||||
Reference in New Issue
Block a user