feat(m1): generative Flaeche — Orb + Karten-Renderer (present_view)
Visueller Renderer fuer aria_view: Orb (reanimated-Puls je Zustand), CardView (text/image/list/map/code, Sci-Fi-Chrome), AriaViewCanvas (pannbare Flaeche, 2-Finger-Pan + Pinch, Karten materialisieren gestaffelt). WorkspaceScreen blendet die Flaeche als Overlay ueber Chat/Cockpit ein, sobald ARIA fuers fokussierte Projekt eine Ansicht komponiert. v1/Vorgeschmack, tsc-clean; Markdown=Klartext, Map=Marker-Liste. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,175 @@
|
|||||||
|
/**
|
||||||
|
* AriaViewCanvas — die pannbare Flaeche, auf der ARIAs komponierte Ansicht
|
||||||
|
* (aria_view) MATERIALISIERT: Orb oben, darunter die Karten. Erscheint als
|
||||||
|
* Overlay ueber dem Chat, sobald ARIA present_view aufruft ("sag was → Orb denkt
|
||||||
|
* → Karte fliegt rein"). Der erste, greifbare Vorgeschmack aufs generative
|
||||||
|
* Cockpit (M1).
|
||||||
|
*
|
||||||
|
* Bedienung (NoMachine-Prinzip): 2-Finger halten + schieben bewegt die Welt,
|
||||||
|
* Pinch zoomt. Ein-Finger-Touch geht an die Karten durch (Scrollen). Die Welt
|
||||||
|
* traegt gerenderte/gestreamte Inhalte — interaktive native Panels rasten
|
||||||
|
* spaeter bei Scale 1 ein (Chat bleibt separat darunter).
|
||||||
|
*
|
||||||
|
* Geraete-agnostisch gehalten: liest nur die ViewSpec, damit ein spaeterer Web-/
|
||||||
|
* AR-Renderer dieselbe Spec konsumieren kann.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
import Animated, {
|
||||||
|
FadeInDown,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useSharedValue,
|
||||||
|
withTiming,
|
||||||
|
} from 'react-native-reanimated';
|
||||||
|
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
||||||
|
import { ViewSpec } from '../services/ariaView';
|
||||||
|
import Orb from './Orb';
|
||||||
|
import CardView from './CardView';
|
||||||
|
|
||||||
|
const MIN_SCALE = 0.5;
|
||||||
|
const MAX_SCALE = 3;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
view: ViewSpec;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AriaViewCanvas: React.FC<Props> = ({ view, onClose }) => {
|
||||||
|
const tx = useSharedValue(0);
|
||||||
|
const ty = useSharedValue(0);
|
||||||
|
const scale = useSharedValue(1);
|
||||||
|
const savedTx = useSharedValue(0);
|
||||||
|
const savedTy = useSharedValue(0);
|
||||||
|
const savedScale = useSharedValue(1);
|
||||||
|
|
||||||
|
const pan = Gesture.Pan()
|
||||||
|
.minPointers(2)
|
||||||
|
.maxPointers(2)
|
||||||
|
.onUpdate((e) => {
|
||||||
|
tx.value = savedTx.value + e.translationX;
|
||||||
|
ty.value = savedTy.value + e.translationY;
|
||||||
|
})
|
||||||
|
.onEnd(() => {
|
||||||
|
savedTx.value = tx.value;
|
||||||
|
savedTy.value = ty.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const pinch = Gesture.Pinch()
|
||||||
|
.onUpdate((e) => {
|
||||||
|
const next = savedScale.value * e.scale;
|
||||||
|
scale.value = Math.max(MIN_SCALE, Math.min(MAX_SCALE, next));
|
||||||
|
})
|
||||||
|
.onEnd(() => {
|
||||||
|
savedScale.value = scale.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const composed = Gesture.Simultaneous(pan, pinch);
|
||||||
|
|
||||||
|
const worldStyle = useAnimatedStyle(() => ({
|
||||||
|
transform: [
|
||||||
|
{ translateX: tx.value },
|
||||||
|
{ translateY: ty.value },
|
||||||
|
{ scale: scale.value },
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const resetCamera = () => {
|
||||||
|
tx.value = withTiming(0);
|
||||||
|
ty.value = withTiming(0);
|
||||||
|
scale.value = withTiming(1);
|
||||||
|
savedTx.value = 0;
|
||||||
|
savedTy.value = 0;
|
||||||
|
savedScale.value = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cards = Array.isArray(view.cards) ? view.cards : [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.overlay}>
|
||||||
|
<GestureDetector gesture={composed}>
|
||||||
|
<Animated.View style={[styles.world, worldStyle]}>
|
||||||
|
<View style={styles.orbWrap}>
|
||||||
|
<Orb state={view.orb} size={110} />
|
||||||
|
</View>
|
||||||
|
{!!view.title && <Text style={styles.worldTitle}>{view.title}</Text>}
|
||||||
|
<View style={styles.cards}>
|
||||||
|
{cards.map((c, i) => (
|
||||||
|
<Animated.View
|
||||||
|
key={i}
|
||||||
|
entering={FadeInDown.duration(420).delay(120 + i * 90)}
|
||||||
|
>
|
||||||
|
<CardView card={c} />
|
||||||
|
</Animated.View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
</GestureDetector>
|
||||||
|
|
||||||
|
{/* Steuerung — ausserhalb des Transforms, immer bei Scale 1 bedienbar */}
|
||||||
|
<View style={styles.topBar} pointerEvents="box-none">
|
||||||
|
<TouchableOpacity style={styles.iconBtn} onPress={resetCamera}>
|
||||||
|
<Text style={styles.icon}>⤢</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity style={styles.iconBtn} onPress={onClose}>
|
||||||
|
<Text style={styles.icon}>✕</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<View style={styles.hintWrap} pointerEvents="none">
|
||||||
|
<Text style={styles.hint}>2 Finger: schieben · Pinch: zoomen</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
overlay: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
backgroundColor: 'rgba(6,6,16,0.94)',
|
||||||
|
zIndex: 50,
|
||||||
|
},
|
||||||
|
world: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingTop: 48,
|
||||||
|
paddingHorizontal: 18,
|
||||||
|
},
|
||||||
|
orbWrap: { marginTop: 8, marginBottom: 6 },
|
||||||
|
worldTitle: {
|
||||||
|
color: '#C9C9FF',
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '700',
|
||||||
|
marginBottom: 4,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
cards: { width: '100%', maxWidth: 560 },
|
||||||
|
topBar: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 10,
|
||||||
|
right: 12,
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
iconBtn: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 20,
|
||||||
|
marginLeft: 10,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: 'rgba(30,30,60,0.9)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(123,92,255,0.4)',
|
||||||
|
},
|
||||||
|
icon: { color: '#C9C9FF', fontSize: 18 },
|
||||||
|
hintWrap: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 14,
|
||||||
|
alignSelf: 'center',
|
||||||
|
},
|
||||||
|
hint: {
|
||||||
|
color: '#6A6A90',
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default AriaViewCanvas;
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* CardView — rendert EINE Karte einer aria_view-Spec (M1). Schaltet nach
|
||||||
|
* card.type auf den passenden Renderer. Unbekannte Typen werden als Text-
|
||||||
|
* Fallback gezeigt (nie crashen).
|
||||||
|
*
|
||||||
|
* Bewusst dependency-leicht (v1): Markdown wird als Klartext dargestellt, Map
|
||||||
|
* als Marker-Liste (kein Karten-Lib), Code als Monospace-Block. Spaeter koennen
|
||||||
|
* einzelne Renderer aufgebohrt werden, ohne die Spec/den Fluss zu aendern.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Image, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||||
|
import { ViewCard, ViewMarker } from '../services/ariaView';
|
||||||
|
|
||||||
|
const ImageBody: React.FC<{ src?: string }> = ({ src }) => {
|
||||||
|
const isUrl = !!src && /^https?:\/\//i.test(src);
|
||||||
|
if (isUrl) {
|
||||||
|
return <Image source={{ uri: src }} style={styles.image} resizeMode="contain" />;
|
||||||
|
}
|
||||||
|
return <Text style={styles.muted}>🖼️ {src || '(kein Bild)'}</Text>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ListBody: React.FC<{ md?: string }> = ({ md }) => {
|
||||||
|
const lines = (md || '')
|
||||||
|
.split('\n')
|
||||||
|
.map((l) => l.replace(/^\s*[-*•]\s?/, '').trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
if (lines.length === 0) return <Text style={styles.muted}>(leer)</Text>;
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{lines.map((l, i) => (
|
||||||
|
<View key={i} style={styles.listRow}>
|
||||||
|
<Text style={styles.bullet}>•</Text>
|
||||||
|
<Text style={styles.text}>{l}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MapBody: React.FC<{ markers?: ViewMarker[] }> = ({ markers }) => {
|
||||||
|
const ms = Array.isArray(markers) ? markers : [];
|
||||||
|
return (
|
||||||
|
<View style={styles.map}>
|
||||||
|
<Text style={styles.mapHint}>🗺️ Karte ({ms.length} Orte)</Text>
|
||||||
|
{ms.map((m, i) => (
|
||||||
|
<Text key={i} style={styles.text}>
|
||||||
|
📍 {m.label || `${m.lat?.toFixed?.(4)}, ${m.lon?.toFixed?.(4)}`}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CodeBody: React.FC<{ md?: string; path?: string; lang?: string }> = ({ md, path, lang }) => (
|
||||||
|
<View>
|
||||||
|
{(path || lang) && (
|
||||||
|
<Text style={styles.codeCaption}>
|
||||||
|
{path || ''}{lang ? ` · ${lang}` : ''}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<ScrollView horizontal style={styles.codeScroll}>
|
||||||
|
<Text style={styles.code}>{md || ''}</Text>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
const CardView: React.FC<{ card: ViewCard }> = ({ card }) => {
|
||||||
|
return (
|
||||||
|
<View style={styles.card}>
|
||||||
|
{!!card.title && <Text style={styles.cardTitle}>{card.title}</Text>}
|
||||||
|
{card.type === 'image' ? (
|
||||||
|
<ImageBody src={card.src} />
|
||||||
|
) : card.type === 'list' ? (
|
||||||
|
<ListBody md={card.md} />
|
||||||
|
) : card.type === 'map' ? (
|
||||||
|
<MapBody markers={card.markers} />
|
||||||
|
) : card.type === 'code' ? (
|
||||||
|
<CodeBody md={card.md} path={card.path} lang={card.lang} />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.text}>{card.md || ''}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
card: {
|
||||||
|
backgroundColor: 'rgba(18,18,42,0.92)',
|
||||||
|
borderColor: 'rgba(123,92,255,0.35)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 14,
|
||||||
|
padding: 14,
|
||||||
|
marginVertical: 8,
|
||||||
|
shadowColor: '#7B5CFF',
|
||||||
|
shadowOpacity: 0.25,
|
||||||
|
shadowRadius: 12,
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
elevation: 6,
|
||||||
|
},
|
||||||
|
cardTitle: { color: '#C9C9FF', fontSize: 15, fontWeight: '700', marginBottom: 8 },
|
||||||
|
text: { color: '#E6E6F0', fontSize: 14, lineHeight: 20, flexShrink: 1 },
|
||||||
|
muted: { color: '#8A8AB0', fontSize: 13, fontStyle: 'italic' },
|
||||||
|
image: { width: '100%', height: 200, borderRadius: 8, backgroundColor: '#0D0D1A' },
|
||||||
|
listRow: { flexDirection: 'row', alignItems: 'flex-start', marginVertical: 2 },
|
||||||
|
bullet: { color: '#7B5CFF', marginRight: 8, fontSize: 14, lineHeight: 20 },
|
||||||
|
map: { backgroundColor: '#0D0D1A', borderRadius: 8, padding: 10 },
|
||||||
|
mapHint: { color: '#00B4D8', fontSize: 13, fontWeight: '600', marginBottom: 6 },
|
||||||
|
codeCaption: { color: '#8A8AB0', fontSize: 12, marginBottom: 6 },
|
||||||
|
codeScroll: { backgroundColor: '#0A0A14', borderRadius: 8, padding: 10 },
|
||||||
|
code: { color: '#B9F5C9', fontFamily: 'monospace', fontSize: 12.5, lineHeight: 18 },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default React.memo(CardView);
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* Orb — ARIAs Praesenz-Avatar (M1). Zeigt ihren Zustand (idle/listening/
|
||||||
|
* thinking/speaking/working) als pulsierender Leucht-Kern und ist das
|
||||||
|
* verbindende Element ueber alle Oberflaechen (App/Web/spaeter Brille).
|
||||||
|
*
|
||||||
|
* Reine Optik, keine Logik — der Zustand kommt von aussen (aria_view.orb bzw.
|
||||||
|
* spaeter direkt von Audio/Wake-Word-Signalen). Dependency-leicht: nur
|
||||||
|
* reanimated (schon installiert), kein SVG/Gradient noetig.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { StyleSheet, View } from 'react-native';
|
||||||
|
import Animated, {
|
||||||
|
Easing,
|
||||||
|
cancelAnimation,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useSharedValue,
|
||||||
|
withRepeat,
|
||||||
|
withTiming,
|
||||||
|
} from 'react-native-reanimated';
|
||||||
|
import { OrbState } from '../services/ariaView';
|
||||||
|
|
||||||
|
const COLORS: Record<OrbState, string> = {
|
||||||
|
idle: '#3A6EA5',
|
||||||
|
listening: '#00B4D8',
|
||||||
|
thinking: '#7B5CFF',
|
||||||
|
speaking: '#34C759',
|
||||||
|
working: '#FF9500',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
state?: OrbState;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Orb: React.FC<Props> = ({ state = 'idle', size = 120 }) => {
|
||||||
|
const pulse = useSharedValue(1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fast = state === 'thinking' || state === 'working';
|
||||||
|
cancelAnimation(pulse);
|
||||||
|
pulse.value = 1;
|
||||||
|
pulse.value = withRepeat(
|
||||||
|
withTiming(fast ? 1.14 : 1.07, {
|
||||||
|
duration: fast ? 620 : 1500,
|
||||||
|
easing: Easing.inOut(Easing.ease),
|
||||||
|
}),
|
||||||
|
-1,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return () => cancelAnimation(pulse);
|
||||||
|
}, [state, pulse]);
|
||||||
|
|
||||||
|
const animStyle = useAnimatedStyle(() => ({ transform: [{ scale: pulse.value }] }));
|
||||||
|
const color = COLORS[state] || COLORS.idle;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.wrap, { width: size, height: size }]}>
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.glow,
|
||||||
|
{ width: size, height: size, borderRadius: size / 2, backgroundColor: color },
|
||||||
|
animStyle,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.ring,
|
||||||
|
{
|
||||||
|
width: size * 0.72,
|
||||||
|
height: size * 0.72,
|
||||||
|
borderRadius: size * 0.36,
|
||||||
|
borderColor: color,
|
||||||
|
},
|
||||||
|
animStyle,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.core,
|
||||||
|
{
|
||||||
|
width: size * 0.44,
|
||||||
|
height: size * 0.44,
|
||||||
|
borderRadius: size * 0.22,
|
||||||
|
backgroundColor: color,
|
||||||
|
shadowColor: color,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
wrap: { alignItems: 'center', justifyContent: 'center' },
|
||||||
|
glow: { position: 'absolute', opacity: 0.22 },
|
||||||
|
ring: { position: 'absolute', borderWidth: 2, opacity: 0.55 },
|
||||||
|
core: {
|
||||||
|
shadowOpacity: 0.9,
|
||||||
|
shadowRadius: 16,
|
||||||
|
shadowOffset: { width: 0, height: 0 },
|
||||||
|
elevation: 12,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default React.memo(Orb);
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { View } from 'react-native';
|
||||||
import projectFocus, { FocusSnapshot } from '../services/projectFocus';
|
import projectFocus, { FocusSnapshot } from '../services/projectFocus';
|
||||||
import codeFile from '../services/codeFile';
|
import codeFile from '../services/codeFile';
|
||||||
import brainApi from '../services/brainApi';
|
import brainApi from '../services/brainApi';
|
||||||
@@ -16,6 +17,8 @@ import viewMode, { ViewModeValue } from '../services/viewMode';
|
|||||||
import ChatScreen from '../screens/ChatScreen';
|
import ChatScreen from '../screens/ChatScreen';
|
||||||
import { TileId } from './layout';
|
import { TileId } from './layout';
|
||||||
import WorkspaceDeck from './WorkspaceDeck';
|
import WorkspaceDeck from './WorkspaceDeck';
|
||||||
|
import ariaView, { AriaView } from '../services/ariaView';
|
||||||
|
import AriaViewCanvas from './AriaViewCanvas';
|
||||||
|
|
||||||
const COCKPIT_PANELS: TileId[] = ['chat', 'files', 'editor', 'vnc'];
|
const COCKPIT_PANELS: TileId[] = ['chat', 'files', 'editor', 'vnc'];
|
||||||
|
|
||||||
@@ -24,12 +27,21 @@ const WorkspaceScreen: React.FC = () => {
|
|||||||
const [focus, setFocus] = useState<FocusSnapshot>(projectFocus.get());
|
const [focus, setFocus] = useState<FocusSnapshot>(projectFocus.get());
|
||||||
const [hasCode, setHasCode] = useState(false);
|
const [hasCode, setHasCode] = useState(false);
|
||||||
const [hasDesktop, setHasDesktop] = useState(false);
|
const [hasDesktop, setHasDesktop] = useState(false);
|
||||||
|
const [view, setView] = useState<AriaView | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => viewMode.subscribe(setMode), []);
|
useEffect(() => viewMode.subscribe(setMode), []);
|
||||||
useEffect(() => projectFocus.subscribe(setFocus), []);
|
useEffect(() => projectFocus.subscribe(setFocus), []);
|
||||||
|
|
||||||
const pid = focus.focusedProjectId;
|
const pid = focus.focusedProjectId;
|
||||||
|
|
||||||
|
// aria_view: ARIAs komponierte Ansicht fuers fokussierte Projekt spiegeln.
|
||||||
|
useEffect(() => {
|
||||||
|
setView(ariaView.getView(pid));
|
||||||
|
return ariaView.subscribe((v) => {
|
||||||
|
if ((v.projectId || '') === (pid || '')) setView(v);
|
||||||
|
});
|
||||||
|
}, [pid]);
|
||||||
|
|
||||||
// Code-Signal: hat der Spiegel schon Dateien fuer dieses Projekt?
|
// Code-Signal: hat der Spiegel schon Dateien fuer dieses Projekt?
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHasCode(codeFile.getFiles(pid).length > 0);
|
setHasCode(codeFile.getFiles(pid).length > 0);
|
||||||
@@ -59,13 +71,32 @@ const WorkspaceScreen: React.FC = () => {
|
|||||||
vnc: hasDesktop ? '#34C759' : undefined,
|
vnc: hasDesktop ? '#34C759' : undefined,
|
||||||
} as Partial<Record<TileId, string>>), [hasCode, hasDesktop]);
|
} as Partial<Record<TileId, string>>), [hasCode, hasDesktop]);
|
||||||
|
|
||||||
// Kompakt-Ansicht: klassischer Vollbild-Chat, exakt wie vor dem Umbau.
|
// Kompakt-Ansicht: klassischer Vollbild-Chat; Cockpit: Workbench mit Dock.
|
||||||
if (mode === 'compact') {
|
const content =
|
||||||
return <ChatScreen />;
|
mode === 'compact' ? (
|
||||||
}
|
<ChatScreen />
|
||||||
|
) : (
|
||||||
|
<WorkspaceDeck projectId={pid} panels={COCKPIT_PANELS} badges={badges} />
|
||||||
|
);
|
||||||
|
|
||||||
// Cockpit: Workbench mit Dock.
|
// Generative Flaeche als Overlay, sobald ARIA fuer dieses Projekt eine Ansicht
|
||||||
return <WorkspaceDeck projectId={pid} panels={COCKPIT_PANELS} badges={badges} />;
|
// komponiert hat (present_view → aria_view). Chat/Cockpit bleiben darunter.
|
||||||
|
const showView = !!view && (view.projectId || '') === (pid || '');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
{content}
|
||||||
|
{showView && view && (
|
||||||
|
<AriaViewCanvas
|
||||||
|
view={view.view}
|
||||||
|
onClose={() => {
|
||||||
|
ariaView.clear(pid);
|
||||||
|
setView(undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default WorkspaceScreen;
|
export default WorkspaceScreen;
|
||||||
|
|||||||
Reference in New Issue
Block a user