enhanced mobile view

This commit is contained in:
ValueOn AG 2026-03-16 22:55:27 +01:00
parent 9c1b9676c8
commit 7e45b6a638
10 changed files with 489 additions and 187 deletions

View file

@ -1,16 +1,21 @@
.container { .container {
display: flex; display: flex;
min-height: 100vh; min-height: 100dvh;
font-family: "DM Sans", sans-serif; font-family: "DM Sans", sans-serif;
color: var(--color-bg); color: var(--color-bg);
} }
@supports not (min-height: 100dvh) {
.container {
min-height: 100vh;
}
}
.mainContent { .mainContent {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 3rem; padding: 2.5rem 2rem;
background-color: var(--color-bg); background-color: var(--color-bg);
} }
@ -18,39 +23,28 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
width: 100%;
flex: 1; flex: 1;
} }
.logoText { .logo {
font-size: 35px;
display: flex; display: flex;
justify-content: center;
align-items: center; align-items: center;
letter-spacing: -0.5px; width: 100%;
font-weight: 200;
} }
.logoPower { .logoImage {
color: var(--color-text); height: 44px;
} width: auto;
object-fit: contain;
.logoOn {
color: var(--color-secondary);
font-weight: 700;
}
.logo img {
height: 40px;
} }
.loginBox { .loginBox {
background-color: var(--color-bg); background-color: var(--color-bg);
width: 25%; width: min(100%, 460px);
height: auto; height: auto;
margin-top: 2rem;
margin-top: 5%;
padding: 2rem; padding: 2rem;
border-radius: 25px; border-radius: 25px;
@ -297,3 +291,41 @@ button:disabled {
.passwordResetLink .textButton:hover { .passwordResetLink .textButton:hover {
color: var(--color-secondary); color: var(--color-secondary);
} }
@media (max-width: 768px) {
.mainContent {
padding: 1rem;
}
.logoImage {
height: 40px;
}
.loginBox {
width: 100%;
margin-top: 1.25rem;
padding: 1.25rem;
border-radius: 20px;
}
.registerLink {
flex-wrap: wrap;
text-align: center;
}
}
@media (max-width: 420px) {
.mainContent {
padding: 0.75rem;
}
.loginBox {
padding: 1rem;
border-radius: 16px;
}
.input,
.button {
height: 48px;
}
}

View file

@ -108,10 +108,11 @@ function Login() {
<div className={styles.container}> <div className={styles.container}>
<div className={styles.mainContent}> <div className={styles.mainContent}>
<div className={styles.logo}> <div className={styles.logo}>
<div className={styles.logoText}> <img
<span className={styles.logoPower}>Power</span> src="/logos/poweron-logo.png"
<span className={styles.logoOn}>On</span> alt="PowerOn"
</div> className={styles.logoImage}
/>
</div> </div>
<div className={styles.loginSection}> <div className={styles.loginSection}>
<div className={styles.loginBox}> <div className={styles.loginBox}>

View file

@ -1,16 +1,21 @@
.container { .container {
display: flex; display: flex;
min-height: 100vh; min-height: 100dvh;
font-family: "DM Sans", sans-serif; font-family: "DM Sans", sans-serif;
color: var(--color-bg); color: var(--color-bg);
} }
@supports not (min-height: 100dvh) {
.container {
min-height: 100vh;
}
}
.mainContent { .mainContent {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 3rem; padding: 2.5rem 2rem;
background-color: var(--color-bg); background-color: var(--color-bg);
} }
@ -18,39 +23,28 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
width: 100%;
flex: 1; flex: 1;
} }
.logoText { .logo {
font-size: 35px;
display: flex; display: flex;
justify-content: center;
align-items: center; align-items: center;
letter-spacing: -0.5px; width: 100%;
font-weight: 200;
} }
.logoPower { .logoImage {
color: var(--color-text); height: 44px;
} width: auto;
object-fit: contain;
.logoOn {
color: var(--color-secondary);
font-weight: 700;
}
.logo img {
height: 40px;
} }
.loginBox { .loginBox {
background-color: var(--color-bg); background-color: var(--color-bg);
width: 25%; width: min(100%, 460px);
height: auto; height: auto;
margin-top: 2rem;
margin-top: 5%;
padding: 2rem; padding: 2rem;
border-radius: 25px; border-radius: 25px;
@ -240,3 +234,41 @@ button:disabled {
.infoMessage p { .infoMessage p {
margin: 0; margin: 0;
} }
@media (max-width: 768px) {
.mainContent {
padding: 1rem;
}
.logoImage {
height: 40px;
}
.loginBox {
width: 100%;
margin-top: 1.25rem;
padding: 1.25rem;
border-radius: 20px;
}
.registerLink {
flex-wrap: wrap;
text-align: center;
}
}
@media (max-width: 420px) {
.mainContent {
padding: 0.75rem;
}
.loginBox {
padding: 1rem;
border-radius: 16px;
}
.input,
.button {
height: 48px;
}
}

View file

@ -54,10 +54,11 @@ function PasswordResetRequest() {
<div className={styles.container}> <div className={styles.container}>
<div className={styles.mainContent}> <div className={styles.mainContent}>
<div className={styles.logo}> <div className={styles.logo}>
<div className={styles.logoText}> <img
<span className={styles.logoPower}>Power</span> src="/logos/poweron-logo.png"
<span className={styles.logoOn}>On</span> alt="PowerOn"
</div> className={styles.logoImage}
/>
</div> </div>
<div className={styles.loginSection}> <div className={styles.loginSection}>
<div className={styles.loginBox}> <div className={styles.loginBox}>

View file

@ -1,16 +1,21 @@
.container { .container {
display: flex; display: flex;
min-height: 100vh; min-height: 100dvh;
font-family: "DM Sans", sans-serif; font-family: "DM Sans", sans-serif;
color: var(--color-bg); color: var(--color-bg);
} }
@supports not (min-height: 100dvh) {
.container {
min-height: 100vh;
}
}
.mainContent { .mainContent {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 3rem; padding: 2.5rem 2rem;
background-color: var(--color-bg); background-color: var(--color-bg);
} }
@ -18,39 +23,28 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
width: 100%;
flex: 1; flex: 1;
} }
.logoText { .logo {
font-size: 35px;
display: flex; display: flex;
justify-content: center;
align-items: center; align-items: center;
letter-spacing: -0.5px; width: 100%;
font-weight: 200;
} }
.logoPower { .logoImage {
color: var(--color-text); height: 44px;
} width: auto;
object-fit: contain;
.logoOn {
color: var(--color-secondary);
font-weight: 700;
}
.logo img {
height: 40px;
} }
.loginBox { .loginBox {
background-color: var(--color-bg); background-color: var(--color-bg);
width: 25%; width: min(100%, 460px);
height: auto; height: auto;
margin-top: 2rem;
margin-top: 5%;
padding: 2rem; padding: 2rem;
border-radius: 25px; border-radius: 25px;
@ -273,3 +267,41 @@ button:disabled {
.infoMessage p { .infoMessage p {
margin: 0; margin: 0;
} }
@media (max-width: 768px) {
.mainContent {
padding: 1rem;
}
.logoImage {
height: 40px;
}
.loginBox {
width: 100%;
margin-top: 1.25rem;
padding: 1.25rem;
border-radius: 20px;
}
.registerLink {
flex-wrap: wrap;
text-align: center;
}
}
@media (max-width: 420px) {
.mainContent {
padding: 0.75rem;
}
.loginBox {
padding: 1rem;
border-radius: 16px;
}
.input,
.button {
height: 48px;
}
}

View file

@ -137,10 +137,11 @@ function Register() {
<div className={styles.container}> <div className={styles.container}>
<div className={styles.mainContent}> <div className={styles.mainContent}>
<div className={styles.logo}> <div className={styles.logo}>
<div className={styles.logoText}> <img
<span className={styles.logoPower}>Power</span> src="/logos/poweron-logo.png"
<span className={styles.logoOn}>On</span> alt="PowerOn"
</div> className={styles.logoImage}
/>
</div> </div>
<div className={styles.loginSection}> <div className={styles.loginSection}>
<div className={styles.loginBox}> <div className={styles.loginBox}>

View file

@ -1,16 +1,21 @@
.container { .container {
display: flex; display: flex;
min-height: 100vh; min-height: 100dvh;
font-family: "DM Sans", sans-serif; font-family: "DM Sans", sans-serif;
color: var(--color-bg); color: var(--color-bg);
} }
@supports not (min-height: 100dvh) {
.container {
min-height: 100vh;
}
}
.mainContent { .mainContent {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 3rem; padding: 2.5rem 2rem;
background-color: var(--color-bg); background-color: var(--color-bg);
} }
@ -18,39 +23,28 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
width: 100%;
flex: 1; flex: 1;
} }
.logoText { .logo {
font-size: 35px;
display: flex; display: flex;
justify-content: center;
align-items: center; align-items: center;
letter-spacing: -0.5px; width: 100%;
font-weight: 200;
} }
.logoPower { .logoImage {
color: var(--color-text); height: 44px;
} width: auto;
object-fit: contain;
.logoOn {
color: var(--color-secondary);
font-weight: 700;
}
.logo img {
height: 40px;
} }
.loginBox { .loginBox {
background-color: var(--color-bg); background-color: var(--color-bg);
width: 25%; width: min(100%, 460px);
height: auto; height: auto;
margin-top: 2rem;
margin-top: 5%;
padding: 2rem; padding: 2rem;
border-radius: 25px; border-radius: 25px;
@ -234,3 +228,41 @@ button:disabled {
font-family: var(--font-family); font-family: var(--font-family);
margin-bottom: 10px; margin-bottom: 10px;
} }
@media (max-width: 768px) {
.mainContent {
padding: 1rem;
}
.logoImage {
height: 40px;
}
.loginBox {
width: 100%;
margin-top: 1.25rem;
padding: 1.25rem;
border-radius: 20px;
}
.registerLink {
flex-wrap: wrap;
text-align: center;
}
}
@media (max-width: 420px) {
.mainContent {
padding: 0.75rem;
}
.loginBox {
padding: 1rem;
border-radius: 16px;
}
.input,
.button {
height: 48px;
}
}

View file

@ -96,10 +96,11 @@ function Reset() {
<div className={styles.container}> <div className={styles.container}>
<div className={styles.mainContent}> <div className={styles.mainContent}>
<div className={styles.logo}> <div className={styles.logo}>
<div className={styles.logoText}> <img
<span className={styles.logoPower}>Power</span> src="/logos/poweron-logo.png"
<span className={styles.logoOn}>On</span> alt="PowerOn"
</div> className={styles.logoImage}
/>
</div> </div>
<div className={styles.loginSection}> <div className={styles.loginSection}>
<div className={styles.loginBox}> <div className={styles.loginBox}>
@ -135,10 +136,11 @@ function Reset() {
<div className={styles.container}> <div className={styles.container}>
<div className={styles.mainContent}> <div className={styles.mainContent}>
<div className={styles.logo}> <div className={styles.logo}>
<div className={styles.logoText}> <img
<span className={styles.logoPower}>Power</span> src="/logos/poweron-logo.png"
<span className={styles.logoOn}>On</span> alt="PowerOn"
</div> className={styles.logoImage}
/>
</div> </div>
<div className={styles.loginSection}> <div className={styles.loginSection}>
<div className={styles.loginBox}> <div className={styles.loginBox}>

View file

@ -44,6 +44,7 @@ interface WorkspaceInputProps {
uploading?: boolean; uploading?: boolean;
selectedProviders?: string[]; selectedProviders?: string[];
onProvidersChange?: (providers: string[]) => void; onProvidersChange?: (providers: string[]) => void;
isMobile?: boolean;
} }
export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
@ -59,6 +60,7 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
uploading = false, uploading = false,
selectedProviders = [], selectedProviders = [],
onProvidersChange, onProvidersChange,
isMobile = false,
}) => { }) => {
const [prompt, setPrompt] = useState(''); const [prompt, setPrompt] = useState('');
const [showAutocomplete, setShowAutocomplete] = useState(false); const [showAutocomplete, setShowAutocomplete] = useState(false);
@ -267,6 +269,8 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
: []; : [];
const hasAttachments = attachedFileIds.length > 0 || attachedDataSourceIds.length > 0; const hasAttachments = attachedFileIds.length > 0 || attachedDataSourceIds.length > 0;
const _horizontalPadding = isMobile ? 12 : 24;
const _controlSize = isMobile ? 38 : 40;
return ( return (
<div style={{ <div style={{
@ -277,7 +281,7 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
{/* Pending uploaded files */} {/* Pending uploaded files */}
{pendingFiles.length > 0 && ( {pendingFiles.length > 0 && (
<div style={{ <div style={{
padding: '6px 24px', padding: `6px ${_horizontalPadding}px`,
display: 'flex', display: 'flex',
gap: 6, gap: 6,
flexWrap: 'wrap', flexWrap: 'wrap',
@ -314,7 +318,7 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
{/* Attachment bar */} {/* Attachment bar */}
{hasAttachments && ( {hasAttachments && (
<div style={{ <div style={{
padding: '6px 24px', padding: `6px ${_horizontalPadding}px`,
display: 'flex', display: 'flex',
gap: 6, gap: 6,
flexWrap: 'wrap', flexWrap: 'wrap',
@ -377,8 +381,8 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
<div style={{ <div style={{
position: 'absolute', position: 'absolute',
bottom: '100%', bottom: '100%',
left: 24, left: _horizontalPadding,
right: 24, right: _horizontalPadding,
maxHeight: 200, maxHeight: 200,
overflowY: 'auto', overflowY: 'auto',
background: '#fff', background: '#fff',
@ -410,7 +414,13 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
)} )}
{/* Main input row */} {/* Main input row */}
<div style={{ padding: '8px 24px 12px', display: 'flex', gap: 8, alignItems: 'flex-end' }}> <div style={{
padding: `8px ${_horizontalPadding}px 12px`,
display: 'flex',
gap: 8,
alignItems: isMobile ? 'stretch' : 'flex-end',
flexWrap: isMobile ? 'wrap' : 'nowrap',
}}>
<textarea <textarea
ref={textareaRef} ref={textareaRef}
value={prompt} value={prompt}
@ -420,7 +430,7 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
disabled={isProcessing} disabled={isProcessing}
style={{ style={{
flex: 1, flex: 1,
minHeight: 40, minHeight: isMobile ? 44 : 40,
maxHeight: 120, maxHeight: 120,
resize: 'vertical', resize: 'vertical',
padding: '10px 14px', padding: '10px 14px',
@ -429,6 +439,7 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
fontSize: 14, fontSize: 14,
fontFamily: 'inherit', fontFamily: 'inherit',
outline: 'none', outline: 'none',
flexBasis: isMobile ? '100%' : undefined,
}} }}
rows={1} rows={1}
/> />
@ -438,7 +449,7 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
disabled={uploading || isProcessing} disabled={uploading || isProcessing}
title="Datei anhängen" title="Datei anhängen"
style={{ style={{
width: 40, height: 40, borderRadius: 8, border: '1px solid var(--border-color, #ddd)', width: _controlSize, height: _controlSize, borderRadius: 8, border: '1px solid var(--border-color, #ddd)',
background: 'var(--secondary-bg, #f5f5f5)', background: 'var(--secondary-bg, #f5f5f5)',
color: uploading ? '#1976d2' : '#666', color: uploading ? '#1976d2' : '#666',
cursor: uploading || isProcessing ? 'not-allowed' : 'pointer', cursor: uploading || isProcessing ? 'not-allowed' : 'pointer',
@ -456,7 +467,7 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
disabled={isProcessing} disabled={isProcessing}
title="Datenquellen anhängen" title="Datenquellen anhängen"
style={{ style={{
width: 40, height: 40, borderRadius: 8, border: '1px solid var(--border-color, #ddd)', width: _controlSize, height: _controlSize, borderRadius: 8, border: '1px solid var(--border-color, #ddd)',
background: attachedDataSourceIds.length > 0 ? '#e8f5e9' : 'var(--secondary-bg, #f5f5f5)', background: attachedDataSourceIds.length > 0 ? '#e8f5e9' : 'var(--secondary-bg, #f5f5f5)',
color: attachedDataSourceIds.length > 0 ? '#2e7d32' : '#666', color: attachedDataSourceIds.length > 0 ? '#2e7d32' : '#666',
cursor: isProcessing ? 'not-allowed' : 'pointer', cursor: isProcessing ? 'not-allowed' : 'pointer',
@ -536,7 +547,7 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
onClick={() => setShowLangPicker(prev => !prev)} onClick={() => setShowLangPicker(prev => !prev)}
title="Sprache waehlen" title="Sprache waehlen"
style={{ style={{
height: 40, borderRadius: '8px 0 0 8px', border: '1px solid var(--border-color, #ddd)', height: _controlSize, borderRadius: '8px 0 0 8px', border: '1px solid var(--border-color, #ddd)',
borderRight: 'none', borderRight: 'none',
background: 'var(--secondary-bg, #f5f5f5)', background: 'var(--secondary-bg, #f5f5f5)',
color: '#666', cursor: 'pointer', fontSize: 10, padding: '0 6px', color: '#666', cursor: 'pointer', fontSize: 10, padding: '0 6px',
@ -549,7 +560,7 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
onClick={_toggleVoice} onClick={_toggleVoice}
title={voiceActive ? 'Aufnahme stoppen' : 'Sprachaufnahme starten'} title={voiceActive ? 'Aufnahme stoppen' : 'Sprachaufnahme starten'}
style={{ style={{
width: 40, height: 40, borderRadius: '0 8px 8px 0', border: 'none', width: _controlSize, height: _controlSize, borderRadius: '0 8px 8px 0', border: 'none',
background: voiceActive ? '#f44336' : 'var(--secondary-bg, #f5f5f5)', background: voiceActive ? '#f44336' : 'var(--secondary-bg, #f5f5f5)',
color: voiceActive ? '#fff' : '#666', color: voiceActive ? '#fff' : '#666',
cursor: 'pointer', fontSize: 18, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', fontSize: 18, display: 'flex', alignItems: 'center', justifyContent: 'center',
@ -589,6 +600,7 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
style={{ style={{
padding: '10px 20px', borderRadius: 8, border: 'none', padding: '10px 20px', borderRadius: 8, border: 'none',
background: '#f44336', color: '#fff', cursor: 'pointer', fontWeight: 600, background: '#f44336', color: '#fff', cursor: 'pointer', fontWeight: 600,
minWidth: isMobile ? 84 : undefined,
}} }}
> >
Stop Stop
@ -601,6 +613,7 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
padding: '10px 20px', borderRadius: 8, border: 'none', padding: '10px 20px', borderRadius: 8, border: 'none',
background: prompt.trim() ? 'var(--primary-color, #1976d2)' : '#ccc', background: prompt.trim() ? 'var(--primary-color, #1976d2)' : '#ccc',
color: '#fff', cursor: prompt.trim() ? 'pointer' : 'default', fontWeight: 600, color: '#fff', cursor: prompt.trim() ? 'pointer' : 'default', fontWeight: 600,
minWidth: isMobile ? 84 : undefined,
}} }}
> >
Send Send

View file

@ -7,7 +7,7 @@
* Right sidebar: FilePreview, ToolActivityLog * Right sidebar: FilePreview, ToolActivityLog
*/ */
import React, { useState, useCallback, useRef } from 'react'; import React, { useState, useCallback, useRef, useEffect } from 'react';
import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
import { useFileOperations } from '../../../hooks/useFiles'; import { useFileOperations } from '../../../hooks/useFiles';
import { useWorkspace } from './useWorkspace'; import { useWorkspace } from './useWorkspace';
@ -80,6 +80,27 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
const [isDragOver, setIsDragOver] = useState(false); const [isDragOver, setIsDragOver] = useState(false);
const dragCounterRef = useRef(0); const dragCounterRef = useRef(0);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [isMobile, setIsMobile] = useState<boolean>(() =>
typeof window !== 'undefined' ? window.innerWidth <= 1024 : false,
);
const [mobileLeftOpen, setMobileLeftOpen] = useState(false);
const [mobileRightOpen, setMobileRightOpen] = useState(false);
useEffect(() => {
const _handleResize = () => {
setIsMobile(window.innerWidth <= 1024);
};
_handleResize();
window.addEventListener('resize', _handleResize);
return () => window.removeEventListener('resize', _handleResize);
}, []);
useEffect(() => {
if (!isMobile) {
setMobileLeftOpen(false);
setMobileRightOpen(false);
}
}, [isMobile]);
const _uploadAndAttach = useCallback(async (file: File) => { const _uploadAndAttach = useCallback(async (file: File) => {
const result = await fileOps.handleFileUpload(file, undefined, instanceId); const result = await fileOps.handleFileUpload(file, undefined, instanceId);
@ -147,6 +168,9 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
setSelectedFileId(fileId); setSelectedFileId(fileId);
setRightTab('preview'); setRightTab('preview');
setRightCollapsed(false); setRightCollapsed(false);
if (isMobile) {
setMobileRightOpen(true);
}
}; };
const _handleConversationSelect = (wfId: string) => { const _handleConversationSelect = (wfId: string) => {
@ -166,10 +190,74 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
textTransform: 'uppercase' as const, textTransform: 'uppercase' as const,
}); });
const _leftPanelBody = (
<>
<div style={{ display: 'flex', borderBottom: '1px solid var(--border-color, #e0e0e0)' }}>
<button style={tabButtonStyle(leftTab === 'conversations')} onClick={() => setLeftTab('conversations')}>Chats</button>
<button style={tabButtonStyle(leftTab === 'files')} onClick={() => setLeftTab('files')}>Files</button>
<button style={tabButtonStyle(leftTab === 'datasources')} onClick={() => setLeftTab('datasources')}>Sources</button>
</div>
<div style={{ flex: 1, overflow: 'auto' }}>
{leftTab === 'conversations' && (
<ConversationList
instanceId={instanceId}
activeWorkflowId={workspace.workflowId}
onSelect={_handleConversationSelect}
onCreateNew={workspace.resetToNew}
refreshTrigger={workspace.workflowVersion}
/>
)}
{leftTab === 'files' && (
<FileBrowser
instanceId={instanceId}
files={workspace.files}
folders={workspace.folders}
onRefresh={workspace.refreshFiles}
onFileSelect={_handleFileSelect}
/>
)}
{leftTab === 'datasources' && (
<DataSourcePanel
instanceId={instanceId}
dataSources={workspace.dataSources}
onRefresh={workspace.refreshDataSources}
/>
)}
</div>
</>
);
const _rightPanelBody = (
<>
<div style={{ padding: '6px 12px', borderBottom: '1px solid var(--border-color, #e0e0e0)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', gap: 8 }}>
<button style={tabButtonStyle(rightTab === 'activity')} onClick={() => setRightTab('activity')}>Activity</button>
<button style={tabButtonStyle(rightTab === 'preview')} onClick={() => setRightTab('preview')}>Preview</button>
</div>
{!isMobile && (
<button onClick={() => setRightCollapsed(true)} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 14, color: '#888' }}></button>
)}
</div>
<div style={{ flex: 1, overflow: 'auto' }}>
{rightTab === 'activity' && (
<ToolActivityLog activities={workspace.toolActivities} />
)}
{rightTab === 'preview' && (
<FilePreview
instanceId={instanceId}
fileId={selectedFileId}
files={workspace.files}
/>
)}
</div>
</>
);
return ( return (
<div style={{ display: 'flex', flex: 1, minHeight: 0, overflow: 'hidden' }}> <div style={{ display: 'flex', flex: 1, minHeight: 0, overflow: 'hidden', position: 'relative' }}>
{/* Left sidebar */} {/* Left sidebar */}
{!leftCollapsed && ( {!isMobile && !leftCollapsed && (
<aside style={{ <aside style={{
width: _leftResize.width, width: _leftResize.width,
minWidth: 200, minWidth: 200,
@ -183,45 +271,12 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
<span style={{ fontWeight: 600, fontSize: 14 }}>Workspace</span> <span style={{ fontWeight: 600, fontSize: 14 }}>Workspace</span>
<button onClick={() => setLeftCollapsed(true)} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 14, color: '#888' }}></button> <button onClick={() => setLeftCollapsed(true)} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 14, color: '#888' }}></button>
</div> </div>
{_leftPanelBody}
<div style={{ display: 'flex', borderBottom: '1px solid var(--border-color, #e0e0e0)' }}>
<button style={tabButtonStyle(leftTab === 'conversations')} onClick={() => setLeftTab('conversations')}>Chats</button>
<button style={tabButtonStyle(leftTab === 'files')} onClick={() => setLeftTab('files')}>Files</button>
<button style={tabButtonStyle(leftTab === 'datasources')} onClick={() => setLeftTab('datasources')}>Sources</button>
</div>
<div style={{ flex: 1, overflow: 'auto' }}>
{leftTab === 'conversations' && (
<ConversationList
instanceId={instanceId}
activeWorkflowId={workspace.workflowId}
onSelect={_handleConversationSelect}
onCreateNew={workspace.resetToNew}
refreshTrigger={workspace.workflowVersion}
/>
)}
{leftTab === 'files' && (
<FileBrowser
instanceId={instanceId}
files={workspace.files}
folders={workspace.folders}
onRefresh={workspace.refreshFiles}
onFileSelect={_handleFileSelect}
/>
)}
{leftTab === 'datasources' && (
<DataSourcePanel
instanceId={instanceId}
dataSources={workspace.dataSources}
onRefresh={workspace.refreshDataSources}
/>
)}
</div>
</aside> </aside>
)} )}
{/* Left resize handle */} {/* Left resize handle */}
{!leftCollapsed && ( {!isMobile && !leftCollapsed && (
<div <div
onMouseDown={e => _leftResize.onMouseDown(e, 1)} onMouseDown={e => _leftResize.onMouseDown(e, 1)}
style={{ width: 4, cursor: 'col-resize', background: 'transparent', flexShrink: 0 }} style={{ width: 4, cursor: 'col-resize', background: 'transparent', flexShrink: 0 }}
@ -230,7 +285,7 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
/> />
)} )}
{leftCollapsed && ( {!isMobile && leftCollapsed && (
<div style={{ width: 32, display: 'flex', alignItems: 'start', justifyContent: 'center', paddingTop: 8, borderRight: '1px solid var(--border-color, #e0e0e0)' }}> <div style={{ width: 32, display: 'flex', alignItems: 'start', justifyContent: 'center', paddingTop: 8, borderRight: '1px solid var(--border-color, #e0e0e0)' }}>
<button onClick={() => setLeftCollapsed(false)} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 14, color: '#888' }}></button> <button onClick={() => setLeftCollapsed(false)} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 14, color: '#888' }}></button>
</div> </div>
@ -247,6 +302,57 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
onDragOver={_handleDragOver} onDragOver={_handleDragOver}
onDrop={_handleDrop} onDrop={_handleDrop}
> >
{isMobile && (
<div style={{
display: 'flex',
gap: 8,
padding: '8px 12px',
borderBottom: '1px solid var(--border-color, #e0e0e0)',
background: 'var(--bg-primary, #fff)',
flexWrap: 'wrap',
}}>
<button
onClick={() => setMobileLeftOpen(true)}
style={{
padding: '6px 10px',
borderRadius: 8,
border: '1px solid var(--border-color, #ddd)',
background: '#f7f7f7',
cursor: 'pointer',
fontSize: 12,
fontWeight: 600,
}}
>
Workspace
</button>
<button
onClick={() => { setRightTab('activity'); setMobileRightOpen(true); }}
style={{
padding: '6px 10px',
borderRadius: 8,
border: '1px solid var(--border-color, #ddd)',
background: rightTab === 'activity' ? '#e8f3ff' : '#f7f7f7',
cursor: 'pointer',
fontSize: 12,
}}
>
Activity
</button>
<button
onClick={() => { setRightTab('preview'); setMobileRightOpen(true); }}
style={{
padding: '6px 10px',
borderRadius: 8,
border: '1px solid var(--border-color, #ddd)',
background: rightTab === 'preview' ? '#e8f3ff' : '#f7f7f7',
cursor: 'pointer',
fontSize: 12,
}}
>
Preview
</button>
</div>
)}
{isDragOver && ( {isDragOver && (
<div style={{ <div style={{
position: 'absolute', inset: 0, zIndex: 100, position: 'absolute', inset: 0, zIndex: 100,
@ -284,11 +390,12 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
uploading={fileOps.uploadingFile} uploading={fileOps.uploadingFile}
selectedProviders={selectedProviders} selectedProviders={selectedProviders}
onProvidersChange={setSelectedProviders} onProvidersChange={setSelectedProviders}
isMobile={isMobile}
/> />
</main> </main>
{/* Right resize handle */} {/* Right resize handle */}
{!rightCollapsed && ( {!isMobile && !rightCollapsed && (
<div <div
onMouseDown={e => _rightResize.onMouseDown(e, -1)} onMouseDown={e => _rightResize.onMouseDown(e, -1)}
style={{ width: 4, cursor: 'col-resize', background: 'transparent', flexShrink: 0 }} style={{ width: 4, cursor: 'col-resize', background: 'transparent', flexShrink: 0 }}
@ -298,7 +405,7 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
)} )}
{/* Right sidebar */} {/* Right sidebar */}
{!rightCollapsed && ( {!isMobile && !rightCollapsed && (
<aside style={{ <aside style={{
width: _rightResize.width, width: _rightResize.width,
minWidth: 200, minWidth: 200,
@ -308,33 +415,82 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
overflow: 'hidden', overflow: 'hidden',
flexShrink: 0, flexShrink: 0,
}}> }}>
<div style={{ padding: '6px 12px', borderBottom: '1px solid var(--border-color, #e0e0e0)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> {_rightPanelBody}
<div style={{ display: 'flex', gap: 8 }}>
<button style={tabButtonStyle(rightTab === 'activity')} onClick={() => setRightTab('activity')}>Activity</button>
<button style={tabButtonStyle(rightTab === 'preview')} onClick={() => setRightTab('preview')}>Preview</button>
</div>
<button onClick={() => setRightCollapsed(true)} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 14, color: '#888' }}></button>
</div>
<div style={{ flex: 1, overflow: 'auto' }}>
{rightTab === 'activity' && (
<ToolActivityLog activities={workspace.toolActivities} />
)}
{rightTab === 'preview' && (
<FilePreview
instanceId={instanceId}
fileId={selectedFileId}
files={workspace.files}
/>
)}
</div>
</aside> </aside>
)} )}
{rightCollapsed && ( {!isMobile && rightCollapsed && (
<div style={{ width: 32, display: 'flex', alignItems: 'start', justifyContent: 'center', paddingTop: 8, borderLeft: '1px solid var(--border-color, #e0e0e0)' }}> <div style={{ width: 32, display: 'flex', alignItems: 'start', justifyContent: 'center', paddingTop: 8, borderLeft: '1px solid var(--border-color, #e0e0e0)' }}>
<button onClick={() => setRightCollapsed(false)} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 14, color: '#888' }}></button> <button onClick={() => setRightCollapsed(false)} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 14, color: '#888' }}></button>
</div> </div>
)} )}
{isMobile && mobileLeftOpen && (
<div
style={{
position: 'absolute',
inset: 0,
background: 'rgba(0, 0, 0, 0.35)',
zIndex: 120,
display: 'flex',
}}
onClick={() => setMobileLeftOpen(false)}
>
<aside
style={{
width: '100%',
maxWidth: 460,
height: '100%',
background: 'var(--bg-primary, #fff)',
borderRight: '1px solid var(--border-color, #e0e0e0)',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}
onClick={e => e.stopPropagation()}
>
<div style={{ padding: '10px 12px', borderBottom: '1px solid var(--border-color, #e0e0e0)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontWeight: 600, fontSize: 14 }}>Workspace</span>
<button onClick={() => setMobileLeftOpen(false)} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 18, color: '#666' }}>×</button>
</div>
{_leftPanelBody}
</aside>
</div>
)}
{isMobile && mobileRightOpen && (
<div
style={{
position: 'absolute',
inset: 0,
background: 'rgba(0, 0, 0, 0.35)',
zIndex: 120,
display: 'flex',
justifyContent: 'flex-end',
}}
onClick={() => setMobileRightOpen(false)}
>
<aside
style={{
width: '100%',
maxWidth: 460,
height: '100%',
background: 'var(--bg-primary, #fff)',
borderLeft: '1px solid var(--border-color, #e0e0e0)',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}
onClick={e => e.stopPropagation()}
>
<div style={{ padding: '10px 12px', borderBottom: '1px solid var(--border-color, #e0e0e0)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontWeight: 600, fontSize: 14 }}>{rightTab === 'activity' ? 'Activity' : 'Preview'}</span>
<button onClick={() => setMobileRightOpen(false)} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 18, color: '#666' }}>×</button>
</div>
{_rightPanelBody}
</aside>
</div>
)}
</div> </div>
); );
}; };