mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-04-24 03:00:15 -04:00
Merge pull request #1380 from jmd1010/web-windows-resizing
Add flex windows sizing to web interface + raw text input fix
This commit is contained in:
@@ -26,7 +26,17 @@
|
||||
let rightColumnWidth = 50;
|
||||
let isDragging = false;
|
||||
|
||||
// Handle resize functionality
|
||||
// Message input height state (percentage values)
|
||||
const DEFAULT_INPUT_HEIGHT = 30; // Default percentage of the left column
|
||||
const MAX_INPUT_HEIGHT = DEFAULT_INPUT_HEIGHT * 2; // Maximum 200% of default height
|
||||
const MIN_SYSTEM_INSTRUCTIONS_HEIGHT = 20; // Minimum percentage for system instructions
|
||||
let messageInputHeight = DEFAULT_INPUT_HEIGHT;
|
||||
let systemInstructionsHeight = 100 - DEFAULT_INPUT_HEIGHT;
|
||||
let isVerticalDragging = false;
|
||||
let initialMouseY = 0; // Track initial mouse position
|
||||
let initialInputHeight = 0; // Track initial input height
|
||||
|
||||
// Handle horizontal resize functionality
|
||||
function startResize(e: MouseEvent | KeyboardEvent) {
|
||||
isDragging = true;
|
||||
e.preventDefault();
|
||||
@@ -57,11 +67,76 @@
|
||||
// Calculate percentage based on mouse position
|
||||
const percentage = ((e.clientX - containerRect.left) / containerWidth) * 100;
|
||||
|
||||
// Apply constraints (left: 25-50%, right: 50-75%)
|
||||
leftColumnWidth = Math.min(Math.max(percentage, 25), 50);
|
||||
// Apply constraints (left: 40-80%, right: 20-60%)
|
||||
leftColumnWidth = Math.min(Math.max(percentage, 40), 80);
|
||||
rightColumnWidth = 100 - leftColumnWidth;
|
||||
}
|
||||
|
||||
// Handle vertical resize functionality
|
||||
function startVerticalResize(e: MouseEvent | KeyboardEvent) {
|
||||
isVerticalDragging = true;
|
||||
e.preventDefault();
|
||||
|
||||
// Store initial mouse position and input height
|
||||
if (e instanceof MouseEvent) {
|
||||
initialMouseY = e.clientY;
|
||||
initialInputHeight = messageInputHeight;
|
||||
}
|
||||
|
||||
// Add event listeners for drag and release
|
||||
window.addEventListener('mousemove', handleVerticalResize);
|
||||
window.addEventListener('mouseup', stopVerticalResize);
|
||||
}
|
||||
|
||||
function handleVerticalKeyDown(e: KeyboardEvent) {
|
||||
// Only respond to Enter or Space key
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
startVerticalResize(e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleVerticalResize(e: MouseEvent) {
|
||||
if (!isVerticalDragging) return;
|
||||
|
||||
// Get container dimensions
|
||||
const leftColumn = document.querySelector('.left-column');
|
||||
if (!leftColumn) return;
|
||||
|
||||
// Get system instructions element to check its actual height
|
||||
const sysInstructions = leftColumn.querySelector('.system-instructions');
|
||||
if (!sysInstructions) return;
|
||||
|
||||
const columnRect = leftColumn.getBoundingClientRect();
|
||||
const columnHeight = columnRect.height;
|
||||
|
||||
// Calculate height change based on mouse movement
|
||||
const mouseDelta = e.clientY - initialMouseY;
|
||||
const deltaPercentage = (mouseDelta / columnHeight) * 100;
|
||||
const newHeight = initialInputHeight + deltaPercentage;
|
||||
|
||||
// Apply constraints to ensure system instructions remain visible
|
||||
const minHeight = DEFAULT_INPUT_HEIGHT * 0.25; // 25% of default
|
||||
const maxHeight = Math.min(MAX_INPUT_HEIGHT, 100 - MIN_SYSTEM_INSTRUCTIONS_HEIGHT); // Max 200% of default or ensure system instructions are visible
|
||||
|
||||
// Calculate new heights
|
||||
const constrainedHeight = Math.min(Math.max(newHeight, minHeight), maxHeight);
|
||||
const newSysInstructionsHeight = 100 - constrainedHeight;
|
||||
|
||||
// Additional safety check - don't allow resize if it would make system instructions too small
|
||||
const sysInstructionsPixelHeight = (columnHeight * newSysInstructionsHeight) / 100;
|
||||
if (sysInstructionsPixelHeight < 100) return; // Don't resize if it would be less than 100px
|
||||
|
||||
// Apply the new heights
|
||||
messageInputHeight = constrainedHeight;
|
||||
systemInstructionsHeight = newSysInstructionsHeight;
|
||||
}
|
||||
|
||||
function stopVerticalResize() {
|
||||
isVerticalDragging = false;
|
||||
window.removeEventListener('mousemove', handleVerticalResize);
|
||||
window.removeEventListener('mouseup', stopVerticalResize);
|
||||
}
|
||||
|
||||
function stopResize() {
|
||||
isDragging = false;
|
||||
window.removeEventListener('mousemove', handleResize);
|
||||
@@ -73,6 +148,8 @@
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleResize);
|
||||
window.removeEventListener('mouseup', stopResize);
|
||||
window.removeEventListener('mousemove', handleVerticalResize);
|
||||
window.removeEventListener('mouseup', stopVerticalResize);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -81,26 +158,30 @@
|
||||
|
||||
<div class="chat-container flex gap-0 p-2 w-full h-screen">
|
||||
<!-- Left Column -->
|
||||
<aside class="flex flex-col gap-2 pr-2" style="width: {leftColumnWidth}%">
|
||||
<!-- Dropdowns Group -->
|
||||
<aside class="flex flex-col gap-2 pr-2 left-column" style="width: {leftColumnWidth}%">
|
||||
<!-- Dropdowns Group with Model Config -->
|
||||
<div class="bg-background/5 p-2 rounded-lg">
|
||||
<div class="rounded-lg bg-background/10">
|
||||
<DropdownGroup />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Config -->
|
||||
<div class="bg-background/5 p-2 rounded-lg">
|
||||
<ModelConfig />
|
||||
</div>
|
||||
|
||||
<!-- Message Input -->
|
||||
<div class="h-[200px] bg-background/5 rounded-lg overflow-hidden">
|
||||
<div class="bg-background/5 rounded-lg overflow-hidden" style="height: {messageInputHeight}%; max-height: {MAX_INPUT_HEIGHT}%">
|
||||
<ChatInput />
|
||||
</div>
|
||||
|
||||
<!-- Vertical Resize Handle -->
|
||||
<button
|
||||
class="vertical-resize-handle"
|
||||
on:mousedown={startVerticalResize}
|
||||
on:keydown={handleVerticalKeyDown}
|
||||
type="button"
|
||||
aria-label="Resize message input and system instructions"
|
||||
></button>
|
||||
|
||||
<!-- System Instructions -->
|
||||
<div class="flex-1 min-h-0 bg-background/5 p-2 rounded-lg">
|
||||
<div class="flex-1 min-h-[100px] bg-background/5 p-2 rounded-lg system-instructions">
|
||||
<div class="h-full flex flex-col">
|
||||
<Textarea
|
||||
bind:value={$systemPrompt}
|
||||
@@ -168,6 +249,7 @@
|
||||
<NoteDrawer />
|
||||
|
||||
<style>
|
||||
/* Horizontal resize handle */
|
||||
.resize-handle {
|
||||
width: 6px;
|
||||
margin: 0 -3px;
|
||||
@@ -205,6 +287,44 @@
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
/* Vertical resize handle */
|
||||
.vertical-resize-handle {
|
||||
height: 6px;
|
||||
margin: -3px 0;
|
||||
width: 100%;
|
||||
cursor: row-resize;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.vertical-resize-handle::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
transition: background-color 0.2s, height 0.2s;
|
||||
}
|
||||
|
||||
.vertical-resize-handle:hover::after,
|
||||
.vertical-resize-handle:focus::after {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.vertical-resize-handle:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.vertical-resize-handle:focus-visible::after {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
|
||||
@@ -377,10 +377,137 @@ async function readFileContent(file: File): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!userInput.trim()) return;
|
||||
|
||||
try {
|
||||
console.log('\n=== Submit Handler Start ===');
|
||||
|
||||
// Store the user input before any processing
|
||||
const inputText = userInput.trim();
|
||||
console.log('Captured user input:', inputText);
|
||||
|
||||
// Handle YouTube URLs with the existing flow
|
||||
if (isYouTubeURL) {
|
||||
console.log('2a. Starting YouTube flow');
|
||||
await processYouTubeURL(inputText);
|
||||
return;
|
||||
}
|
||||
|
||||
// For regular text input, add the user message to the UI first
|
||||
messageStore.update(messages => [...messages, {
|
||||
role: 'user',
|
||||
content: inputText
|
||||
}]);
|
||||
|
||||
// Add loading indicator
|
||||
messageStore.update(messages => [...messages, {
|
||||
role: 'system',
|
||||
content: 'Processing...',
|
||||
format: 'loading'
|
||||
}]);
|
||||
|
||||
// Clear input fields
|
||||
userInput = "";
|
||||
const filesForProcessing = [...uploadedFiles];
|
||||
const contentsForProcessing = [...fileContents];
|
||||
uploadedFiles = [];
|
||||
fileContents = [];
|
||||
fileButtonKey = !fileButtonKey;
|
||||
|
||||
// Prepare content with file attachments if any
|
||||
const contentWithFiles = contentsForProcessing.length > 0
|
||||
? `${inputText}\n\nFile Contents (${filesForProcessing.map(f => f.endsWith('.pdf') ? 'PDF' : 'Text').join(', ')}):\n${contentsForProcessing.join('\n\n---\n\n')}`
|
||||
: inputText;
|
||||
|
||||
// Get the enhanced prompt
|
||||
const enhancedPrompt = contentsForProcessing.length > 0
|
||||
? `${$systemPrompt}\nAnalyze and process the provided content according to these instructions.`
|
||||
: $systemPrompt;
|
||||
|
||||
console.log('Content to send:', {
|
||||
text: contentWithFiles.substring(0, 100) + '...',
|
||||
length: contentWithFiles.length,
|
||||
hasFiles: contentsForProcessing.length > 0
|
||||
});
|
||||
|
||||
try {
|
||||
// Get the chat stream
|
||||
const stream = await chatService.streamChat(contentWithFiles, enhancedPrompt);
|
||||
|
||||
// Process the stream
|
||||
await chatService.processStream(
|
||||
stream,
|
||||
(content, response) => {
|
||||
messageStore.update(messages => {
|
||||
const newMessages = [...messages];
|
||||
// Remove the loading message
|
||||
const loadingIndex = newMessages.findIndex(m => m.format === 'loading');
|
||||
if (loadingIndex !== -1) {
|
||||
newMessages.splice(loadingIndex, 1);
|
||||
}
|
||||
|
||||
// Add or update the assistant message
|
||||
const assistantIndex = newMessages.findIndex(m => m.role === 'assistant');
|
||||
if (assistantIndex !== -1) {
|
||||
newMessages[assistantIndex].content = content;
|
||||
newMessages[assistantIndex].format = response?.format;
|
||||
} else {
|
||||
newMessages.push({
|
||||
role: 'assistant',
|
||||
content,
|
||||
format: response?.format
|
||||
});
|
||||
}
|
||||
return newMessages;
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
// Make sure to remove loading message on error
|
||||
messageStore.update(messages =>
|
||||
messages.filter(m => m.format !== 'loading')
|
||||
);
|
||||
console.error('Stream processing error:', error);
|
||||
|
||||
// Show error message using a valid format type
|
||||
messageStore.update(messages => [...messages, {
|
||||
role: 'system',
|
||||
content: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
format: 'plain'
|
||||
}]);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
// Make sure to remove loading message on error
|
||||
messageStore.update(messages =>
|
||||
messages.filter(m => m.format !== 'loading')
|
||||
);
|
||||
throw error; // Re-throw to be caught by the outer try/catch
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Chat submission error:', error);
|
||||
|
||||
// Make sure to remove loading message on error (redundant but safe)
|
||||
messageStore.update(messages =>
|
||||
messages.filter(m => m.format !== 'loading')
|
||||
);
|
||||
|
||||
// Show error message using a valid format type
|
||||
messageStore.update(messages => [...messages, {
|
||||
role: 'system',
|
||||
content: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
format: 'plain'
|
||||
}]);
|
||||
} finally {
|
||||
// As a final safety measure, ensure loading message is removed
|
||||
messageStore.update(messages =>
|
||||
messages.filter(m => m.format !== 'loading')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async function handleSubmit() {
|
||||
/* async function handleSubmit() {
|
||||
if (!userInput.trim()) return;
|
||||
|
||||
try {
|
||||
@@ -403,29 +530,28 @@ async function handleSubmit() {
|
||||
format: 'loading'
|
||||
}]);
|
||||
|
||||
userInput = ""; // Reset userInput BEFORE sendMessage
|
||||
uploadedFiles = []; // Reset uploadedFiles BEFORE sendMessage
|
||||
fileContents = []; // Reset fileContents BEFORE sendMessage
|
||||
fileButtonKey = !fileButtonKey; // Toggle key to force re-creation
|
||||
|
||||
|
||||
// Store the user input before clearing it
|
||||
const inputText = userInput;
|
||||
|
||||
// Construct finalContent BEFORE clearing userInput
|
||||
const finalContent = fileContents.length > 0
|
||||
? `${userInput}\n\nFile Contents (${uploadedFiles.map(f => f.endsWith('.pdf') ? 'PDF' : 'Text').join(', ')}):\n${fileContents.join('\n\n---\n\n')}`
|
||||
: userInput;
|
||||
? `${inputText}\n\nFile Contents (${uploadedFiles.map(f => f.endsWith('.pdf') ? 'PDF' : 'Text').join(', ')}):\n${fileContents.join('\n\n---\n\n')}`
|
||||
: inputText;
|
||||
|
||||
// Now clear the input fields
|
||||
userInput = "";
|
||||
uploadedFiles = [];
|
||||
fileContents = [];
|
||||
fileButtonKey = !fileButtonKey;
|
||||
|
||||
|
||||
|
||||
|
||||
await sendMessage(finalContent, enhancedPrompt);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Chat submission error:', error);
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
|
||||
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
|
||||
@@ -35,10 +35,13 @@
|
||||
$: if ($chatState.messages.length > 0) {
|
||||
const lastMessage = $chatState.messages[$chatState.messages.length - 1];
|
||||
isUserMessage = lastMessage.role === 'user';
|
||||
if (isUserMessage) {
|
||||
// Only auto-scroll on user messages
|
||||
setTimeout(scrollToBottom, 100);
|
||||
}
|
||||
// Auto-scroll on both user messages and assistant messages
|
||||
setTimeout(scrollToBottom, 100);
|
||||
}
|
||||
|
||||
// Also watch for streaming state changes to ensure scrolling when streaming completes
|
||||
$: if ($streamingStore === false) {
|
||||
setTimeout(scrollToBottom, 100);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Patterns from "./Patterns.svelte";
|
||||
import Models from "./Models.svelte";
|
||||
import ModelConfig from "./ModelConfig.svelte";
|
||||
import { Select } from "$lib/components/ui/select";
|
||||
import { languageStore } from '$lib/store/language-store';
|
||||
|
||||
@@ -15,21 +16,29 @@
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="w-[50%]">
|
||||
<Patterns />
|
||||
<div class="flex gap-4">
|
||||
<!-- Left side - Dropdowns -->
|
||||
<div class="w-[35%] flex flex-col gap-3">
|
||||
<div>
|
||||
<Patterns />
|
||||
</div>
|
||||
<div>
|
||||
<Models />
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
bind:value={$languageStore}
|
||||
class="bg-primary-800/30 border-none hover:bg-primary-800/40 transition-colors"
|
||||
>
|
||||
{#each languages as lang}
|
||||
<option value={lang.code}>{lang.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-[50%]">
|
||||
<Models />
|
||||
</div>
|
||||
<div class="w-[50%]">
|
||||
<Select
|
||||
bind:value={$languageStore}
|
||||
class="bg-primary-800/30 border-none hover:bg-primary-800/40 transition-colors"
|
||||
>
|
||||
{#each languages as lang}
|
||||
<option value={lang.code}>{lang.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
|
||||
<!-- Right side - Model Config -->
|
||||
<div class="w-[65%]">
|
||||
<ModelConfig />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user