Compare commits

...

17 Commits

Author SHA1 Message Date
github-actions[bot]
45d06f8854 chore(release): Update version to v1.4.364 2025-12-28 21:00:26 +00:00
Kayvan Sylvan
fdc64c8fd6 Merge pull request #1907 from majiayu000/feat/gui-session-support
feat(gui): add Session Name support for multi-turn conversations
2025-12-28 12:57:52 -08:00
Changelog Bot
8ae93940f3 chore: incoming 1907 changelog entry 2025-12-28 12:50:44 -08:00
Changelog Bot
cc5d232cfe chore: incoming 1907 changelog entry 2025-12-28 12:40:49 -08:00
lif
a6e9d6ae92 fix(gui): fix Select binding and empty input handling
- Use bind:value for proper two-way binding with Select component
- Handle empty input to clear session when user clears the field
- Skip session change if value unchanged to avoid redundant API calls
- Track previous session to restore when placeholder selected

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 10:34:14 +08:00
lif
e0b70d2d90 refactor(gui): extract SessionSelector component and address PR feedback
- Extract session UI into dedicated SessionSelector.svelte component
- Use Select component instead of native <select>
- Add session message loading when selecting existing session
- Fix placeholder selection behavior to preserve current session
- Rename "Session ID" to "Session Name" for consistency
- Add proper error handling for session loading
- Simplify reactive statements with nullish coalescing
- Use ?? instead of || in ChatService.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 10:26:04 +08:00
Changelog Bot
b3993238d5 chore: incoming 1907 changelog entry 2025-12-27 11:14:55 -08:00
lif
5f5728ee8e fix(gui): fix Session ID input and improve layout
- Remove reactive statement that was resetting input on each keystroke
- Initialize sessionInput only once in onMount
- Change layout to stack input and dropdown vertically for better display

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 08:51:56 +08:00
lif
6c5487609e feat(gui): add Session ID support for multi-turn conversations
Add session name parameter to GUI chat interface, enabling persistent
multi-turn conversations similar to CLI's --session flag.

Changes:
- Add SessionName field to PromptRequest in chat.go
- Add sessionName to ChatPrompt interface
- Include currentSession in ChatService requests
- Add Session ID input with existing sessions dropdown in DropdownGroup

Closes #680

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 08:11:30 +08:00
github-actions[bot]
79241d9335 chore(release): Update version to v1.4.363 2025-12-25 16:13:41 +00:00
Kayvan Sylvan
2fedd1fd86 Merge pull request #1906 from ksylvan/kayvan/christmas-2025-code-cleanups
Code Quality: Optimize HTTP client reuse + simplify error formatting
2025-12-25 08:11:11 -08:00
Changelog Bot
a8a8fa05c9 chore: incoming 1906 changelog entry 2025-12-25 08:04:30 -08:00
Kayvan Sylvan
33130f2087 refactor: optimize HTTP client reuse and simplify error formatting
### CHANGES

- Simplify error wrapping by removing redundant Sprintf calls in CLI
- Pass HTTP client to FetchModelsDirectly to enable connection reuse
- Store persistent HTTP client instance inside the OpenAI provider struct
- Update compatible AI providers to match the new function signature
- Add error handling for pattern loading from absolute file paths
2025-12-25 07:58:49 -08:00
github-actions[bot]
d5f84224eb chore(release): Update version to v1.4.362 2025-12-25 05:08:47 +00:00
Kayvan Sylvan
14ab79835e Merge pull request #1904 from majiayu000/fix/webui-tooltips-rendering-1790
fix: resolve WebUI tooltips not rendering due to overflow clipping
2025-12-24 21:05:29 -08:00
Changelog Bot
4d0e1e7201 - Add incoming 1904 changelog entry
- Extract positioning calculations into dedicated `positioning.ts` module
- Add reactive tooltip position updates on scroll/resize
- Improve accessibility with `aria-describedby` and unique IDs
- Add SSR safety with `isBrowser` flag check
- Replace inline position calculation with reactive statement
- Add window event listeners for position tracking
- Update unit tests to use extracted functions
- Add test coverage for style formatting function
2025-12-24 21:01:08 -08:00
majiayu000
257721280f fix: resolve WebUI tooltips not rendering due to overflow clipping
Use position: fixed and getBoundingClientRect() to calculate tooltip
position dynamically. This prevents tooltips from being clipped by
parent containers with overflow: hidden (such as slide transitions).

Closes #1790

Signed-off-by: majiayu000 <majiayu000@users.noreply.github.com>
2025-12-25 11:35:37 +08:00
20 changed files with 308 additions and 44 deletions

View File

@@ -1,5 +1,35 @@
# Changelog
## v1.4.364 (2025-12-28)
### PR [#1907](https://github.com/danielmiessler/Fabric/pull/1907) by [majiayu000](https://github.com/majiayu000): feat(gui): add Session Name support for multi-turn conversations
- Added Session Name support for multi-turn conversations in GUI chat interface, enabling persistent conversations similar to CLI's --session flag
- Added SessionName field to PromptRequest and sessionName to ChatPrompt interface for proper session handling
- Extracted SessionSelector component with Select component instead of native dropdown for better user experience
- Implemented session message loading when selecting existing sessions with proper error handling
- Fixed Select component binding and empty input handling to prevent redundant API calls and properly clear sessions
## v1.4.363 (2025-12-25)
### PR [#1906](https://github.com/danielmiessler/Fabric/pull/1906) by [ksylvan](https://github.com/ksylvan): Code Quality: Optimize HTTP client reuse + simplify error formatting
- Refactor: optimize HTTP client reuse and simplify error formatting
- Simplify error wrapping by removing redundant Sprintf calls in CLI
- Pass HTTP client to FetchModelsDirectly to enable connection reuse
- Store persistent HTTP client instance inside the OpenAI provider struct
- Update compatible AI providers to match the new function signature
## v1.4.362 (2025-12-25)
### PR [#1904](https://github.com/danielmiessler/Fabric/pull/1904) by [majiayu000](https://github.com/majiayu000): fix: resolve WebUI tooltips not rendering due to overflow clipping
- Fix: resolve WebUI tooltips not rendering due to overflow clipping by using position: fixed and getBoundingClientRect() to calculate tooltip position dynamically, preventing tooltips from being clipped by parent containers with overflow: hidden
- Refactor: extract tooltip positioning logic into separate positioning.ts module for better code organization and maintainability
- Improve accessibility with aria-describedby attributes and unique IDs for better screen reader support
- Add reactive tooltip position updates on scroll and resize events for dynamic positioning
- Add SSR safety with isBrowser flag check and comprehensive unit test coverage for the positioning functions
## v1.4.361 (2025-12-25)
### PR [#1905](https://github.com/danielmiessler/Fabric/pull/1905) by [majiayu000](https://github.com/majiayu000): fix: optimize oversized logo images reducing package size by 93%

View File

@@ -1,3 +1,3 @@
package main
var version = "v1.4.361"
var version = "v1.4.364"

Binary file not shown.

View File

@@ -14,19 +14,19 @@ import (
func CopyToClipboard(message string) (err error) {
if err = clipboard.WriteAll(message); err != nil {
err = fmt.Errorf("%s", fmt.Sprintf(i18n.T("could_not_copy_to_clipboard"), err))
err = fmt.Errorf(i18n.T("could_not_copy_to_clipboard"), err)
}
return
}
func CreateOutputFile(message string, fileName string) (err error) {
if _, err = os.Stat(fileName); err == nil {
err = fmt.Errorf("%s", fmt.Sprintf(i18n.T("file_already_exists_not_overwriting"), fileName))
err = fmt.Errorf(i18n.T("file_already_exists_not_overwriting"), fileName)
return
}
var file *os.File
if file, err = os.Create(fileName); err != nil {
err = fmt.Errorf("%s", fmt.Sprintf(i18n.T("error_creating_file"), err))
err = fmt.Errorf(i18n.T("error_creating_file"), err)
return
}
defer file.Close()
@@ -34,7 +34,7 @@ func CreateOutputFile(message string, fileName string) (err error) {
message += "\n"
}
if _, err = file.WriteString(message); err != nil {
err = fmt.Errorf("%s", fmt.Sprintf(i18n.T("error_writing_to_file"), err))
err = fmt.Errorf(i18n.T("error_writing_to_file"), err)
} else {
debuglog.Log("\n\n[Output also written to %s]\n", fileName)
}
@@ -51,13 +51,13 @@ func CreateAudioOutputFile(audioData []byte, fileName string) (err error) {
// File existence check is now done in the CLI layer before TTS generation
var file *os.File
if file, err = os.Create(fileName); err != nil {
err = fmt.Errorf("%s", fmt.Sprintf(i18n.T("error_creating_audio_file"), err))
err = fmt.Errorf(i18n.T("error_creating_audio_file"), err)
return
}
defer file.Close()
if _, err = file.Write(audioData); err != nil {
err = fmt.Errorf("%s", fmt.Sprintf(i18n.T("error_writing_audio_data"), err))
err = fmt.Errorf(i18n.T("error_writing_audio_data"), err)
}
// No redundant output message here - the CLI layer handles success messaging
return

View File

@@ -30,7 +30,8 @@ const maxResponseSize = 10 * 1024 * 1024 // 10MB
// standard OpenAI SDK method fails due to a nonstandard format. This is useful
// for providers that return a direct array of models (e.g., GitHub Models) or
// other OpenAI-compatible implementations.
func FetchModelsDirectly(ctx context.Context, baseURL, apiKey, providerName string) ([]string, error) {
// If httpClient is nil, a new client with default settings will be created.
func FetchModelsDirectly(ctx context.Context, baseURL, apiKey, providerName string, httpClient *http.Client) ([]string, error) {
if ctx == nil {
ctx = context.Background()
}
@@ -52,10 +53,12 @@ func FetchModelsDirectly(ctx context.Context, baseURL, apiKey, providerName stri
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
req.Header.Set("Accept", "application/json")
// TODO: Consider reusing a single http.Client instance (e.g., as a field on Client) instead of allocating a new one for
// each request.
client := &http.Client{
Timeout: 10 * time.Second,
// Reuse provided HTTP client, or create a new one if not provided
client := httpClient
if client == nil {
client = &http.Client{
Timeout: 10 * time.Second,
}
}
resp, err := client.Do(req)
if err != nil {

View File

@@ -3,8 +3,10 @@ package openai
import (
"context"
"fmt"
"net/http"
"slices"
"strings"
"time"
"github.com/danielmiessler/fabric/internal/chat"
"github.com/danielmiessler/fabric/internal/domain"
@@ -65,6 +67,7 @@ type Client struct {
ApiBaseURL *plugins.SetupQuestion
ApiClient *openai.Client
ImplementsResponses bool // Whether this provider supports the Responses API
httpClient *http.Client
}
// SetResponsesAPIEnabled configures whether to use the Responses API
@@ -79,6 +82,11 @@ func (o *Client) configure() (ret error) {
}
client := openai.NewClient(opts...)
o.ApiClient = &client
// Initialize HTTP client for direct API calls (reused across requests)
o.httpClient = &http.Client{
Timeout: 10 * time.Second,
}
return
}
@@ -96,7 +104,7 @@ func (o *Client) ListModels() (ret []string, err error) {
// Some providers (e.g., GitHub Models) return non-standard response formats
// that the SDK fails to parse.
debuglog.Debug(debuglog.Basic, "SDK Models.List failed for %s: %v, falling back to direct API fetch\n", o.GetName(), err)
return FetchModelsDirectly(context.Background(), o.ApiBaseURL.Value, o.ApiKey.Value, o.GetName())
return FetchModelsDirectly(context.Background(), o.ApiBaseURL.Value, o.ApiKey.Value, o.GetName(), o.httpClient)
}
func (o *Client) SendStream(

View File

@@ -20,7 +20,7 @@ func TestFetchModelsDirectly_DirectArray(t *testing.T) {
}))
defer srv.Close()
models, err := FetchModelsDirectly(context.Background(), srv.URL, "test-key", "TestProvider")
models, err := FetchModelsDirectly(context.Background(), srv.URL, "test-key", "TestProvider", nil)
assert.NoError(t, err)
assert.Equal(t, 1, len(models))
assert.Equal(t, "github-model", models[0])
@@ -36,7 +36,7 @@ func TestFetchModelsDirectly_OpenAIFormat(t *testing.T) {
}))
defer srv.Close()
models, err := FetchModelsDirectly(context.Background(), srv.URL, "test-key", "TestProvider")
models, err := FetchModelsDirectly(context.Background(), srv.URL, "test-key", "TestProvider", nil)
assert.NoError(t, err)
assert.Equal(t, 1, len(models))
assert.Equal(t, "openai-model", models[0])
@@ -52,7 +52,7 @@ func TestFetchModelsDirectly_EmptyArray(t *testing.T) {
}))
defer srv.Close()
models, err := FetchModelsDirectly(context.Background(), srv.URL, "test-key", "TestProvider")
models, err := FetchModelsDirectly(context.Background(), srv.URL, "test-key", "TestProvider", nil)
assert.NoError(t, err)
assert.Equal(t, 0, len(models))
}

View File

@@ -9,5 +9,5 @@ import (
// DirectlyGetModels is used to fetch models directly from the API when the
// standard OpenAI SDK method fails due to a nonstandard format.
func (c *Client) DirectlyGetModels(ctx context.Context) ([]string, error) {
return openai.FetchModelsDirectly(ctx, c.ApiBaseURL.Value, c.ApiKey.Value, c.GetName())
return openai.FetchModelsDirectly(ctx, c.ApiBaseURL.Value, c.ApiKey.Value, c.GetName(), nil)
}

View File

@@ -47,7 +47,7 @@ func (c *Client) ListModels() ([]string, error) {
}
// TODO: Handle context properly in Fabric by accepting and propagating a context.Context
// instead of creating a new one here.
return openai.FetchModelsDirectly(context.Background(), c.modelsURL, c.Client.ApiKey.Value, c.GetName())
return openai.FetchModelsDirectly(context.Background(), c.modelsURL, c.Client.ApiKey.Value, c.GetName(), nil)
}
// First try the standard OpenAI SDK approach

View File

@@ -65,7 +65,9 @@ func (o *PatternsEntity) loadPattern(source string) (pattern *Pattern, err error
}
// Use the resolved absolute path to get the pattern
pattern, _ = o.getFromFile(absPath)
if pattern, err = o.getFromFile(absPath); err != nil {
return nil, fmt.Errorf("could not load pattern from file %s: %w", absPath, err)
}
} else {
// Otherwise, get the pattern from the database
pattern, err = o.getFromDB(source)

View File

@@ -29,6 +29,7 @@ type PromptRequest struct {
ContextName string `json:"contextName"`
PatternName string `json:"patternName"`
StrategyName string `json:"strategyName"` // Optional strategy name
SessionName string `json:"sessionName"` // Session name for multi-turn conversations
Variables map[string]string `json:"variables,omitempty"` // Pattern variables
}
@@ -131,6 +132,7 @@ func (h *ChatHandler) HandleChat(c *gin.Context) {
},
PatternName: p.PatternName,
ContextName: p.ContextName,
SessionName: p.SessionName, // Pass session name for multi-turn conversations
PatternVariables: p.Variables, // Pass pattern variables
Language: request.Language, // Pass the language field
}

View File

@@ -1 +1 @@
"1.4.361"
"1.4.364"

View File

@@ -2,8 +2,8 @@
import Patterns from "./Patterns.svelte";
import Models from "./Models.svelte";
import ModelConfig from "./ModelConfig.svelte";
import SessionSelector from "./SessionSelector.svelte";
import { Select } from "$lib/components/ui/select";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { languageStore } from '$lib/store/language-store';
import { strategies, selectedStrategy, fetchStrategies } from '$lib/store/strategy-store';
@@ -75,6 +75,7 @@
{/each}
</Select>
</div>
<SessionSelector />
<div>
<Label for="pattern-variables" class="text-xs text-white/70 mb-1 block">Pattern Variables (JSON)</Label>
<textarea

View File

@@ -0,0 +1,82 @@
<script lang="ts">
import { Select } from "$lib/components/ui/select";
import { Label } from "$lib/components/ui/label";
import { currentSession, setSession, messageStore } from '$lib/store/chat-store';
import { sessionAPI, sessions } from '$lib/store/session-store';
import { onMount } from 'svelte';
let sessionInput = '';
$: sessionsList = $sessions?.map(s => s.Name) ?? [];
function handleSessionInput() {
const trimmed = sessionInput.trim();
if (trimmed) {
setSession(trimmed);
} else {
// Clear session when input is empty
sessionInput = '';
setSession(null);
}
}
let previousSessionInput = '';
async function handleSessionSelect() {
// If the placeholder option (empty value) is selected, restore to previous value
if (!sessionInput) {
sessionInput = previousSessionInput || $currentSession || '';
return;
}
// Skip if session hasn't changed
if (sessionInput === $currentSession) {
return;
}
previousSessionInput = sessionInput;
setSession(sessionInput);
// Load the selected session's message history so the chat reflects prior context
try {
const messages = await sessionAPI.loadSessionMessages(sessionInput);
messageStore.set(messages);
} catch (error) {
console.error('Failed to load session messages:', error);
}
}
onMount(async () => {
try {
await sessionAPI.loadSessions();
} catch (error) {
console.error('Failed to load sessions:', error);
}
sessionInput = $currentSession ?? '';
});
</script>
<div>
<Label for="session-input" class="text-xs text-white/70 mb-1 block">Session Name</Label>
<input
id="session-input"
type="text"
bind:value={sessionInput}
on:blur={handleSessionInput}
on:keydown={(e) => e.key === 'Enter' && handleSessionInput()}
placeholder="Enter session name..."
class="w-full px-3 py-2 text-sm bg-primary-800/30 border-none rounded-md hover:bg-primary-800/40 transition-colors text-white placeholder-white/50 focus:ring-1 focus:ring-white/20 focus:outline-none"
/>
{#if sessionsList.length > 0}
<Select
bind:value={sessionInput}
on:change={handleSessionSelect}
class="mt-2 bg-primary-800/30 border-none hover:bg-primary-800/40 transition-colors"
>
<option value="">Load existing session...</option>
{#each sessionsList as session}
<option value={session}>{session}</option>
{/each}
</Select>
{/if}
</div>

View File

@@ -1,43 +1,86 @@
<script lang="ts">
import { onMount } from 'svelte';
import { calculateTooltipPosition, formatPositionStyle, type TooltipPosition } from './positioning';
export let text: string;
export let position: 'top' | 'bottom' | 'left' | 'right' = 'top';
// biome-ignore lint/style/useConst: Svelte props must use 'let' even when not reassigned
export let position: TooltipPosition = 'top';
let tooltipVisible = false;
let tooltipElement: HTMLDivElement;
// eslint-disable-next-line no-unassigned-vars -- Assigned via bind:this in template
let triggerElement: HTMLDivElement;
let isBrowser = false;
// biome-ignore lint/correctness/noUnusedVariables: Used in template for aria-describedby and id
const tooltipId = `tooltip-${Math.random().toString(36).substring(2, 9)}`;
// Reactive tooltip positioning - recalculates when position or element changes
$: tooltipStyle = triggerElement && tooltipVisible
? formatPositionStyle(calculateTooltipPosition(triggerElement.getBoundingClientRect(), position))
: '';
function updatePosition() {
if (triggerElement && tooltipVisible) {
tooltipStyle = formatPositionStyle(calculateTooltipPosition(triggerElement.getBoundingClientRect(), position));
}
}
// biome-ignore lint/correctness/noUnusedVariables: Used in template event handlers
function showTooltip() {
tooltipVisible = true;
}
// biome-ignore lint/correctness/noUnusedVariables: Used in template event handlers
function hideTooltip() {
tooltipVisible = false;
}
// Handle window scroll and resize to keep tooltip positioned correctly
// Only runs in browser (not during SSR)
onMount(() => {
isBrowser = true;
return () => {
if (isBrowser) {
window.removeEventListener('scroll', updatePosition, true);
window.removeEventListener('resize', updatePosition);
}
};
});
// Add/remove event listeners reactively when tooltip visibility changes
$: if (isBrowser && tooltipVisible) {
window.addEventListener('scroll', updatePosition, true);
window.addEventListener('resize', updatePosition);
} else if (isBrowser && !tooltipVisible) {
window.removeEventListener('scroll', updatePosition, true);
window.removeEventListener('resize', updatePosition);
}
</script>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions a11y-mouse-events-have-key-events -->
<div class="tooltip-container">
<div
<div
bind:this={triggerElement}
class="tooltip-trigger"
on:mouseenter={showTooltip}
on:mouseleave={hideTooltip}
on:focusin={showTooltip}
on:focusout={hideTooltip}
role="tooltip"
aria-label="Tooltip trigger"
aria-describedby={tooltipVisible ? tooltipId : undefined}
role="button"
tabindex="0"
>
<slot />
</div>
{#if tooltipVisible}
<div
bind:this={tooltipElement}
class="tooltip absolute z-[9999] px-2 py-1 text-xs rounded bg-gray-900/90 text-white whitespace-nowrap shadow-lg backdrop-blur-sm"
id={tooltipId}
class="tooltip fixed z-[9999] px-2 py-1 text-xs rounded bg-gray-900/90 text-white whitespace-nowrap shadow-lg backdrop-blur-sm"
class:top="{position === 'top'}"
class:bottom="{position === 'bottom'}"
class:left="{position === 'left'}"
class:right="{position === 'right'}"
style={tooltipStyle}
role="tooltip"
aria-label={text}
>
{text}
<div class="tooltip-arrow" role="presentation" />
@@ -57,32 +100,24 @@
.tooltip {
pointer-events: none;
transition: all 150ms ease-in-out;
transition: opacity 150ms ease-in-out;
opacity: 1;
}
.tooltip.top {
bottom: calc(100% + 5px);
left: 50%;
transform: translateX(-50%);
transform: translate(-50%, -100%);
}
.tooltip.bottom {
top: calc(100% + 5px);
left: 50%;
transform: translateX(-50%);
transform: translate(-50%, 0);
}
.tooltip.left {
right: calc(100% + 5px);
top: 50%;
transform: translateY(-50%);
transform: translate(-100%, -50%);
}
.tooltip.right {
left: calc(100% + 5px);
top: 50%;
transform: translateY(-50%);
transform: translate(0, -50%);
}
.tooltip-arrow {

View File

@@ -0,0 +1,56 @@
import { describe, expect, it } from 'vitest';
import { calculateTooltipPosition, formatPositionStyle, TOOLTIP_GAP } from './positioning';
describe('Tooltip positioning logic', () => {
const mockRect = {
top: 100,
bottom: 130,
left: 200,
right: 300,
width: 100,
height: 30,
x: 200,
y: 100,
toJSON: () => ({})
} as DOMRect;
it('calculates top position correctly', () => {
const result = calculateTooltipPosition(mockRect, 'top');
expect(result.top).toBe(92); // 100 - 8
expect(result.left).toBe(250); // 200 + 100/2
});
it('calculates bottom position correctly', () => {
const result = calculateTooltipPosition(mockRect, 'bottom');
expect(result.top).toBe(138); // 130 + 8
expect(result.left).toBe(250); // 200 + 100/2
});
it('calculates left position correctly', () => {
const result = calculateTooltipPosition(mockRect, 'left');
expect(result.top).toBe(115); // 100 + 30/2
expect(result.left).toBe(192); // 200 - 8
});
it('calculates right position correctly', () => {
const result = calculateTooltipPosition(mockRect, 'right');
expect(result.top).toBe(115); // 100 + 30/2
expect(result.left).toBe(308); // 300 + 8
});
it('uses the correct gap value', () => {
expect(TOOLTIP_GAP).toBe(8);
});
it('formats position style correctly', () => {
const position = { top: 100, left: 200 };
const style = formatPositionStyle(position);
expect(style).toBe('top: 100px; left: 200px;');
});
it('respects custom gap parameter', () => {
const customGap = 16;
const result = calculateTooltipPosition(mockRect, 'top', customGap);
expect(result.top).toBe(84); // 100 - 16
});
});

View File

@@ -0,0 +1,27 @@
export const TOOLTIP_GAP = 8;
export type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';
export interface Position {
top: number;
left: number;
}
export function calculateTooltipPosition(
rect: DOMRect,
position: TooltipPosition,
gap: number = TOOLTIP_GAP
): Position {
const positions: Record<TooltipPosition, Position> = {
top: { top: rect.top - gap, left: rect.left + rect.width / 2 },
bottom: { top: rect.bottom + gap, left: rect.left + rect.width / 2 },
left: { top: rect.top + rect.height / 2, left: rect.left - gap },
right: { top: rect.top + rect.height / 2, left: rect.right + gap }
};
return positions[position];
}
export function formatPositionStyle(position: Position): string {
return `top: ${position.top}px; left: ${position.left}px;`;
}

View File

@@ -8,6 +8,7 @@ export interface ChatPrompt {
model: string;
patternName?: string;
strategyName?: string; // Optional strategy name to prepend strategy prompt
sessionName?: string; // Session name for multi-turn conversations
variables?: { [key: string]: string }; // Pattern variables
}

View File

@@ -14,6 +14,7 @@ import {
systemPrompt,
} from "$lib/store/pattern-store";
import { selectedStrategy } from "$lib/store/strategy-store";
import { currentSession } from "$lib/store/chat-store";
class LanguageValidator {
constructor(private targetLanguage: string) {}
@@ -210,6 +211,7 @@ export class ChatService {
model: config.model,
patternName: get(selectedPatternName),
strategyName: get(selectedStrategy), // Add selected strategy to prompt
sessionName: get(currentSession) ?? undefined, // Session name for multi-turn conversations
variables: get(patternVariables), // Add pattern variables
};
}

View File

@@ -89,5 +89,20 @@ export const sessionAPI = {
toastService.error(error instanceof Error ? error.message : 'Failed to import session');
throw error;
}
},
async loadSessionMessages(sessionName: string): Promise<Message[]> {
try {
const response = await fetch(`/api/sessions/${sessionName}`);
if (!response.ok) {
throw new Error(`Failed to load session: ${response.statusText}`);
}
const data = await response.json();
const messages = Array.isArray(data.Message) ? data.Message : [];
return messages;
} catch (error) {
console.error(`Error loading session messages for ${sessionName}:`, error);
throw error;
}
}
};