diff --git a/apps/stage-tamagotchi/src-tauri/capabilities/default.json b/apps/stage-tamagotchi/src-tauri/capabilities/default.json index 5eacc4097..62d349ae5 100644 --- a/apps/stage-tamagotchi/src-tauri/capabilities/default.json +++ b/apps/stage-tamagotchi/src-tauri/capabilities/default.json @@ -5,12 +5,14 @@ "windows": [ "main", "settings", - "chat" + "chat", + "onboarding" ], "permissions": [ "core:default", "os:default", "core:window:default", + "core:window:allow-close", "core:window:allow-start-dragging", "core:window:allow-set-focus", "core:window:allow-set-minimizable", diff --git a/apps/stage-tamagotchi/src-tauri/src/app/commands/mod.rs b/apps/stage-tamagotchi/src-tauri/src/app/commands/mod.rs index 5764f9d53..3a70cc27e 100644 --- a/apps/stage-tamagotchi/src-tauri/src/app/commands/mod.rs +++ b/apps/stage-tamagotchi/src-tauri/src/app/commands/mod.rs @@ -1,7 +1,7 @@ use log::debug; use tauri::Manager; -use crate::app::windows::{chat, settings}; +use crate::app::windows::{chat, onboarding, settings}; #[tauri::command] pub async fn open_settings_window(app: tauri::AppHandle) -> Result<(), tauri::Error> { @@ -27,6 +27,18 @@ pub async fn open_chat_window(app: tauri::AppHandle) -> Result<(), tauri::Error> Ok(()) } +#[tauri::command] +pub async fn open_onboarding_window(app: tauri::AppHandle) -> Result<(), tauri::Error> { + let window = app.get_webview_window("onboarding"); + if let Some(window) = window { + let _ = window.show(); + return Ok(()); + } + + onboarding::new_onboarding_window(&app)?; + Ok(()) +} + #[tauri::command] pub fn debug_println(msg: serde_json::Value) -> Result<(), tauri::Error> { debug!("{msg}"); diff --git a/apps/stage-tamagotchi/src-tauri/src/app/windows/mod.rs b/apps/stage-tamagotchi/src-tauri/src/app/windows/mod.rs index 122f382af..b3f65570e 100644 --- a/apps/stage-tamagotchi/src-tauri/src/app/windows/mod.rs +++ b/apps/stage-tamagotchi/src-tauri/src/app/windows/mod.rs @@ -1,2 +1,3 @@ pub mod chat; +pub mod onboarding; pub mod settings; diff --git a/apps/stage-tamagotchi/src-tauri/src/app/windows/onboarding.rs b/apps/stage-tamagotchi/src-tauri/src/app/windows/onboarding.rs new file mode 100644 index 000000000..c255b77f8 --- /dev/null +++ b/apps/stage-tamagotchi/src-tauri/src/app/windows/onboarding.rs @@ -0,0 +1,37 @@ +use std::path::Path; + +use anyhow::{Ok, Result}; +#[cfg(target_os = "macos")] +use tauri::TitleBarStyle; +use tauri::{Runtime, WebviewUrl, WebviewWindowBuilder}; + +pub fn new_onboarding_window( + app: &tauri::AppHandle +) -> Result> { + let mut builder = WebviewWindowBuilder::new( + app, + "onboarding", + WebviewUrl::App(Path::new("#/onboarding").to_path_buf()), + ) + .title("Onboarding") + .inner_size(800.0, 600.0) + .shadow(true) + .transparent(false) + .accept_first_mouse(true); + + #[cfg(target_os = "macos")] + { + // macOS traffic light (red, yellow, green) position customization + // + // feat: traffic light position (#12366) ยท tauri-apps/tauri@30f5a15 + // https://github.com/tauri-apps/tauri/commit/30f5a1553d3c0ce460c9006764200a9210915a44 + builder = builder.hidden_title(true); + builder = builder.decorations(true); + builder = builder.title_bar_style(TitleBarStyle::Overlay); + builder = builder.traffic_light_position(tauri::LogicalPosition::new(14.0, 20.0)); + } + + let window = builder.build()?; + + Ok(window) +} diff --git a/apps/stage-tamagotchi/src-tauri/src/lib.rs b/apps/stage-tamagotchi/src-tauri/src/lib.rs index eddb6324c..f763d4b41 100644 --- a/apps/stage-tamagotchi/src-tauri/src/lib.rs +++ b/apps/stage-tamagotchi/src-tauri/src/lib.rs @@ -175,6 +175,7 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ app::commands::open_settings_window, app::commands::open_chat_window, + app::commands::open_onboarding_window, app::commands::debug_println, ]) .run(tauri::generate_context!()) diff --git a/apps/stage-tamagotchi/src-tauri/src/plugins/window_router_link/mod.rs b/apps/stage-tamagotchi/src-tauri/src/plugins/window_router_link/mod.rs index 045b527b8..7ccee2b24 100644 --- a/apps/stage-tamagotchi/src-tauri/src/plugins/window_router_link/mod.rs +++ b/apps/stage-tamagotchi/src-tauri/src/plugins/window_router_link/mod.rs @@ -6,7 +6,7 @@ use tauri::{ plugin::{Builder, TauriPlugin}, }; -use crate::app::windows::{chat, settings}; +use crate::app::windows::{chat, onboarding, settings}; #[tauri::command] #[specta::specta] @@ -31,6 +31,11 @@ async fn go( None => settings::new_settings_window(app) .map_err(|e| format!("Failed to create settings window: {}", e))?, }, + Some("onboarding") => match app.get_webview_window("onboarding") { + Some(window) => window, + None => onboarding::new_onboarding_window(app) + .map_err(|e| format!("Failed to create onboarding window: {}", e))?, + }, Some(label) => { return Err(format!("Unknown window label: {}", label)); }, @@ -46,7 +51,14 @@ async fn go( current_url.set_fragment(Some(route.to_string().as_str())); let _ = target_window.show(); - let _ = target_window.navigate(current_url); + if let Ok(url) = target_window.url() { + if url == current_url { + // If the URL is already the same, we don't need to navigate again. + return Ok(()); + } + + let _ = target_window.navigate(current_url); + } Ok(()) } diff --git a/apps/stage-tamagotchi/src/App.vue b/apps/stage-tamagotchi/src/App.vue index fc3a559a2..a1721c253 100644 --- a/apps/stage-tamagotchi/src/App.vue +++ b/apps/stage-tamagotchi/src/App.vue @@ -9,6 +9,7 @@ import { computed, onMounted, watch } from 'vue' import { useI18n } from 'vue-i18n' import { RouterView } from 'vue-router' +import { WindowRouterLink } from './commands' import { useAppRuntime } from './composables/runtime' import { useTauriEvent } from './composables/tauri' import { useWindowControlStore } from './stores/window-controls' @@ -18,6 +19,7 @@ const i18n = useI18n() const windowControlStore = useWindowControlStore() const mcpStore = useMcpStore() const onboardingStore = useOnboardingStore() +const { shouldShowSetup } = storeToRefs(onboardingStore) const { platform } = useAppRuntime() const { listen } = useTauriEvent() @@ -44,6 +46,12 @@ watch(themeColorsHueDynamic, () => { document.documentElement.classList.toggle('dynamic-hue', themeColorsHueDynamic.value) }, { immediate: true }) +watch(shouldShowSetup, () => { + if (shouldShowSetup.value) { + WindowRouterLink.commands.go('/onboarding', 'onboarding') + } +}) + onMounted(() => { listen('mcp_plugin_destroyed', () => { mcpStore.connected = false diff --git a/apps/stage-tamagotchi/src/composables/tauri-window-persistence.ts b/apps/stage-tamagotchi/src/composables/tauri-window-persistence.ts deleted file mode 100644 index 47c7539fc..000000000 --- a/apps/stage-tamagotchi/src/composables/tauri-window-persistence.ts +++ /dev/null @@ -1,467 +0,0 @@ -import type { Monitor } from '@tauri-apps/api/window' - -import type { DisplayInfo, Point, Size } from './tauri' - -import { useThrottleFn, watchThrottled } from '@vueuse/core' -import { computed, readonly, ref } from 'vue' - -import { useAppRuntime } from './runtime' -import { useTauriWindow } from './tauri' -import { useTauriPointAndWindowFrame } from './tauri-window-pass-through-on-hover' -import { useTauriWindowState } from './tauri-window-state' - -export interface WindowPersistenceConfig { - autoSave?: boolean - autoRestore?: boolean - savePeriod?: number // milliseconds - constrainToDisplays?: boolean - centerPointConstraint?: boolean // Use center point for boundary checks -} - -export interface WindowBoundaryConstraints { - minCenterX: number - maxCenterX: number - minCenterY: number - maxCenterY: number - recommendedMonitor: Monitor | null -} - -/** - * Enhanced window positioning system with persistence and boundary management - * Integrates with the existing tauri-click-through system - */ -export function useWindowPersistence(config: WindowPersistenceConfig = {}) { - const { - autoSave = true, - autoRestore = true, - savePeriod = 10000, - constrainToDisplays = true, - centerPointConstraint = true, - } = config - - const { platform } = useAppRuntime() - const { windowFrame } = useTauriPointAndWindowFrame() - const { saveWindowState, restoreStateCurrent } = useTauriWindowState() - const { setPosition, getAvailableMonitors, getPrimaryMonitor } = useTauriWindow() - - // Debounced save function (declare early to avoid usage before definition) - const throttledSave = useThrottleFn(() => savePosition(), savePeriod) - - // Reactive state - const displayInfo = ref() - const currentWindowPosition = computed(() => windowFrame.value.origin) - const currentWindowSize = computed(() => windowFrame.value.size) - const isPositioning = ref(false) - const isRestoring = ref(false) - - // State tracking (using Tauri plugins only) - const isStateRestored = ref(false) - - const currentMonitor = computed(() => { - if (!currentWindowPosition.value || !displayInfo.value) - return null - return findMonitorContainingCenterPoint( - displayInfo.value.monitors, - getWindowCenterPoint({ - x: currentWindowPosition.value.x, - y: currentWindowPosition.value.y, - }, { - width: currentWindowSize.value.width, - height: currentWindowSize.value.height, - }), - ) - }) - - const boundaryConstraints = computed((): WindowBoundaryConstraints | null => { - if (!displayInfo.value || !currentWindowPosition.value) - return null - - const allMonitors = displayInfo.value.monitors - const windowSize = { - width: currentWindowSize.value.width, - height: currentWindowSize.value.height, - } - - return calculateBoundaryConstraints(allMonitors, windowSize, centerPointConstraint) - }) - - const isWindowOutOfBounds = computed(() => { - if (!currentWindowPosition.value || !displayInfo.value) - return false - - // Check if window center is within any monitor's work area - const center = getWindowCenterPoint(currentWindowPosition.value, currentWindowSize.value) - const isInAnyMonitor = displayInfo.value.monitors.some((monitor) => { - const workArea = monitor.workArea - return ( - center.x >= workArea.position.x - && center.x <= workArea.position.x + workArea.size.width - && center.y >= workArea.position.y - && center.y <= workArea.position.y + workArea.size.height - ) - }) - - return !isInAnyMonitor - }) - - // Initialize the system - async function initialize() { - if (platform.value === 'web') - return - - try { - // Get initial display information - refreshDisplayInfo() - - // Set up event listeners - setupEventListeners() - - // Auto-restore if enabled - if (autoRestore) { - restorePosition() - } - - // Success - using console.warn to comply with linting rules - } - catch (error) { - console.error('[WindowPersistence] Failed to initialize:', error) - } - } - - function setupEventListeners() { - watchThrottled(windowFrame, () => { - if (!isPositioning.value && !isRestoring.value && autoSave) { - throttledSave() - } - }, { deep: true, throttle: savePeriod }) - } - - // Core positioning functions - function savePosition(): boolean { - if (!currentWindowPosition.value || !displayInfo.value) { - return false - } - - try { - saveWindowState() - - return true - } - catch (error) { - console.error('[WindowPersistence] Failed to save position:', error) - return false - } - } - - async function restorePosition(): Promise { - if (!displayInfo.value) { - return false - } - - try { - isRestoring.value = true - - // Restore window state using Tauri plugin - await restoreStateCurrent() - - // Ensure the restored position is within bounds - if (constrainToDisplays && currentWindowPosition.value) { - if (isWindowOutOfBounds.value) { - console.warn('[WindowPersistence] Restored position is not valid for current displays') - await moveToNearestValidPosition() - return false - } - } - - isStateRestored.value = true - return true - } - catch (error) { - console.error('[WindowPersistence] Failed to restore position:', error) - return false - } - finally { - isRestoring.value = false - } - } - - async function ensureWindowInBounds(): Promise { - if (!currentWindowPosition.value || !constrainToDisplays) - return true - - try { - const constrainedPosition = constrainPositionToBounds({ - x: currentWindowPosition.value.x, - y: currentWindowPosition.value.y, - }, { - width: currentWindowSize.value.width, - height: currentWindowSize.value.height, - }) - - if (!positionsEqual({ - x: currentWindowPosition.value.x, - y: currentWindowPosition.value.y, - width: currentWindowSize.value.width, - height: currentWindowSize.value.height, - }, { - x: constrainedPosition.x, - y: constrainedPosition.y, - width: currentWindowSize.value.width, - height: currentWindowSize.value.height, - })) { - await applyPosition(constrainedPosition) - return true - } - - return true - } - catch (error) { - console.error('[WindowPersistence] Failed to ensure window bounds:', error) - return false - } - } - - async function applyPosition(pos: Point): Promise { - if (platform.value === 'web') - return false - - try { - isPositioning.value = true - await setPosition(pos.x, pos.y) - - if (autoSave) { - throttledSave() - } - - return true - } - catch (error) { - console.error('[WindowPersistence] Failed to apply position:', error) - return false - } - finally { - isPositioning.value = false - } - } - - async function centerOnNearestMonitor(): Promise { - if (!displayInfo.value || !currentWindowPosition.value || !currentWindowSize.value) - return false - - // Find the nearest monitor to current window center - const currentCenter = getWindowCenterPoint(currentWindowPosition.value, currentWindowSize.value) - let nearestMonitor = displayInfo.value.monitors[0] - let shortestDistance = Infinity - - displayInfo.value.monitors.forEach((monitor) => { - const monitorCenter = { - x: monitor.workArea.position.x + monitor.workArea.size.width / 2, - y: monitor.workArea.position.y + monitor.workArea.size.height / 2, - } - - const distance = Math.sqrt( - (currentCenter.x - monitorCenter.x) ** 2 - + (currentCenter.y - monitorCenter.y) ** 2, - ) - - if (distance < shortestDistance) { - shortestDistance = distance - nearestMonitor = monitor - } - }) - - // Center window on the nearest monitor - const centerPosition: Point = { - x: nearestMonitor.workArea.position.x + (nearestMonitor.workArea.size.width - currentWindowSize.value.width) / 2, - y: nearestMonitor.workArea.position.y + (nearestMonitor.workArea.size.height - currentWindowSize.value.height) / 2, - } - - return await applyPosition(centerPosition) - } - - async function moveToNearestValidPosition(): Promise { - if (!displayInfo.value || !currentWindowPosition.value || !currentWindowSize.value) - return false - - // If window is already in bounds, no need to move - if (!isWindowOutOfBounds.value) - return true - - // First try to constrain to nearest valid position - const constrainedPosition = constrainPositionToBounds( - currentWindowPosition.value, - currentWindowSize.value, - ) - - // If constraining worked, apply the position - if (constrainedPosition.x !== currentWindowPosition.value.x - || constrainedPosition.y !== currentWindowPosition.value.y) { - return await applyPosition(constrainedPosition) - } - - // If constraining didn't work, center on nearest monitor - return await centerOnNearestMonitor() - } - - // Utility functions - function getWindowCenterPoint(pos: Point, size: Size): Point { - return { - x: pos.x + size.width / 2, - y: pos.y + size.height / 2, - } - } - - function findMonitorContainingCenterPoint(monitors: Monitor[], center: Point): Monitor | null { - return monitors.find((monitor) => { - const workArea = monitor.workArea - return ( - center.x >= workArea.position.x - && center.x <= workArea.position.x + workArea.size.width - && center.y >= workArea.position.y - && center.y <= workArea.position.y + workArea.size.height - ) - }) || null - } - - function calculateBoundaryConstraints( - monitors: Monitor[], - windowSize: Size, - useCenterPoint: boolean, - ): WindowBoundaryConstraints { - if (monitors.length === 0) { - return { - minCenterX: 0, - maxCenterX: 0, - minCenterY: 0, - maxCenterY: 0, - recommendedMonitor: null, - } - } - - // Calculate the combined bounds of all monitors - let minX = Infinity - let maxX = -Infinity - let minY = Infinity - let maxY = -Infinity - let bestMonitor = monitors[0] - - monitors.forEach((monitor) => { - const workArea = monitor.workArea - minX = Math.min(minX, workArea.position.x) - maxX = Math.max(maxX, workArea.position.x + workArea.size.width) - minY = Math.min(minY, workArea.position.y) - maxY = Math.max(maxY, workArea.position.y + workArea.size.height) - - // Use primary monitor from displayInfo instead of is_primary field - if (displayInfo.value && monitor.name === displayInfo.value.primaryMonitor.name) { - bestMonitor = monitor - } - }) - - if (useCenterPoint) { - // Calculate constraints for center point - return { - minCenterX: minX + windowSize.width / 2, - maxCenterX: maxX - windowSize.width / 2, - minCenterY: minY + windowSize.height / 2, - maxCenterY: maxY - windowSize.height / 2, - recommendedMonitor: bestMonitor, - } - } - else { - // Calculate constraints for top-left corner - return { - minCenterX: minX, - maxCenterX: maxX - windowSize.width, - minCenterY: minY, - maxCenterY: maxY - windowSize.height, - recommendedMonitor: bestMonitor, - } - } - } - - function constrainPositionToBounds(pos: Point, size: Size): Point { - if (!boundaryConstraints.value) - return { x: pos.x, y: pos.y } - - const constraints = boundaryConstraints.value - const center = getWindowCenterPoint(pos, size) - - // Constrain center point - const constrainedCenter: Point = { - x: Math.max(constraints.minCenterX, Math.min(constraints.maxCenterX, center.x)), - y: Math.max(constraints.minCenterY, Math.min(constraints.maxCenterY, center.y)), - } - - // Convert back to position - return { - x: constrainedCenter.x - size.width / 2, - y: constrainedCenter.y - size.height / 2, - } - } - - function positionsEqual(a: (Point & Size), b: (Point & Size)): boolean { - if (!a || !b) - return a === b - - return ( - Math.abs(a.x - b.x) < 1 - && Math.abs(a.y - b.y) < 1 - && Math.abs(a.width - b.width) < 1 - && Math.abs(a.height - b.height) < 1 - ) - } - - async function refreshDisplayInfo(): Promise { - if (platform.value === 'web') - return - - try { - const monitors = await getAvailableMonitors() - if (!monitors || monitors.length === 0) { - console.warn('[WindowPersistence] Failed to get available monitors') - return - } - - const primaryMonitor = await getPrimaryMonitor() - if (!primaryMonitor) { - console.warn('[WindowPersistence] Failed to get primary monitor') - return - } - - displayInfo.value = { - monitors, - primaryMonitor, - } - } - catch (error) { - console.error('[WindowPersistence] Failed to refresh display info:', error) - } - } - - return { - // State - displayInfo: readonly(displayInfo), - - // Computed properties for external use - isWindowOutOfBounds, - currentMonitor, - boundaryConstraints, - - // Core functions - initialize, - - // Position correction functions - moveToNearestValidPosition, - centerOnNearestMonitor, - ensureWindowInBounds, - applyPosition, - - // Utilities - refreshDisplayInfo, - - // Manual control functions - manualSave: () => savePosition(), - manualRestore: () => restorePosition(), - } -} diff --git a/apps/stage-tamagotchi/src/composables/tauri.ts b/apps/stage-tamagotchi/src/composables/tauri.ts index 915d25489..843215661 100644 --- a/apps/stage-tamagotchi/src/composables/tauri.ts +++ b/apps/stage-tamagotchi/src/composables/tauri.ts @@ -270,10 +270,34 @@ export function useTauriWindow() { } } + async function closeWindow(label?: string) { + try { + const imported = await _ensureImported() + if (!label) { + const window = imported.getCurrentWindow() + return await window.close() + } + else { + const windows = await imported.getAllWindows() + const targetWindow = windows.find(win => win.label === label) + if (targetWindow) { + return await targetWindow.close() + } + else { + console.warn(`No window found with label: ${label}`) + } + } + } + catch (error) { + console.error('Failed to close window:', error) + } + } + return { getAvailableMonitors, getCurrentMonitor, getPrimaryMonitor, setPosition, + closeWindow, } } diff --git a/apps/stage-tamagotchi/src/layouts/plain.vue b/apps/stage-tamagotchi/src/layouts/plain.vue new file mode 100644 index 000000000..a78fb6fd3 --- /dev/null +++ b/apps/stage-tamagotchi/src/layouts/plain.vue @@ -0,0 +1,7 @@ + + + diff --git a/apps/stage-tamagotchi/src/pages/index.vue b/apps/stage-tamagotchi/src/pages/index.vue index 22429eb5f..5ffe4b47c 100644 --- a/apps/stage-tamagotchi/src/pages/index.vue +++ b/apps/stage-tamagotchi/src/pages/index.vue @@ -13,7 +13,6 @@ import ResourceStatusIsland from '../components/Widgets/ResourceStatusIsland/ind import { useTauriCore, useTauriEvent } from '../composables/tauri' import { useTauriGlobalShortcuts } from '../composables/tauri-global-shortcuts' import { useTauriWindowClickThrough } from '../composables/tauri-window-pass-through-on-hover' -import { useWindowPersistence } from '../composables/tauri-window-persistence' import { useResourcesStore } from '../stores/resources' import { useWindowControlStore } from '../stores/window-controls' import { WindowControlMode } from '../types/window-controls' @@ -37,13 +36,6 @@ const { connected, serverCmd, serverArgs } = storeToRefs(mcpStore) watch([live2dLookAtX, live2dLookAtY], ([x, y]) => live2dFocusAt.value = { x, y }, { immediate: true }) -const windowPersistence = useWindowPersistence({ - autoSave: true, - autoRestore: true, - constrainToDisplays: true, - centerPointConstraint: true, -}) - const modeIndicatorClass = computed(() => { switch (windowStore.controlMode) { case WindowControlMode.MOVE: @@ -60,7 +52,6 @@ const modeIndicatorClass = computed(() => { onMounted(async () => { await invoke('plugin:proj-airi-tauri-plugin-window-pass-through-on-hover|start_monitor') await startClickThrough() - await windowPersistence.initialize() }) onUnmounted(async () => { diff --git a/apps/stage-tamagotchi/src/pages/onboarding.vue b/apps/stage-tamagotchi/src/pages/onboarding.vue new file mode 100644 index 000000000..27fcbeb39 --- /dev/null +++ b/apps/stage-tamagotchi/src/pages/onboarding.vue @@ -0,0 +1,34 @@ + + + + + +meta: + layout: plain + diff --git a/cspell.config.yaml b/cspell.config.yaml index fa0b2b2c2..70e7988de 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -123,6 +123,7 @@ words: - micvad - mineflayer - mingcute + - minimizable - mkdist - modelcontextprotocol - modnet diff --git a/packages/stage-ui/src/components/Widgets/Dialogs/Onboarding/MobileOnboarding.vue b/packages/stage-ui/src/components/Widgets/Dialogs/Onboarding/MobileOnboarding.vue deleted file mode 100644 index eee5c3243..000000000 --- a/packages/stage-ui/src/components/Widgets/Dialogs/Onboarding/MobileOnboarding.vue +++ /dev/null @@ -1,439 +0,0 @@ - - - - - diff --git a/packages/stage-ui/src/components/Widgets/Dialogs/Onboarding/Onboarding.vue b/packages/stage-ui/src/components/Widgets/Dialogs/Onboarding/Onboarding.vue index e51492317..c045d9006 100644 --- a/packages/stage-ui/src/components/Widgets/Dialogs/Onboarding/Onboarding.vue +++ b/packages/stage-ui/src/components/Widgets/Dialogs/Onboarding/Onboarding.vue @@ -3,11 +3,12 @@ import { useProvidersStore } from '@proj-airi/stage-ui/stores' import { FieldInput } from '@proj-airi/ui' import { useDebounceFn } from '@vueuse/core' import { storeToRefs } from 'pinia' -import { computed, onMounted, ref, watch } from 'vue' +import { computed, nextTick, onMounted, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' import onboardingLogo from '../../../../assets/onboarding.png' +import { Callout } from '../../../Layouts' import { RadioCardDetail } from '../../../Menu' import { Button } from '../../../Misc' import { ProviderAccountIdInput } from '../../../Providers' @@ -19,6 +20,9 @@ interface Emits { const emit = defineEmits() +const step = ref(1) +const direction = ref<'next' | 'previous'>('next') + const { t } = useI18n() const providersStore = useProvidersStore() const { providers, allChatProvidersMetadata } = storeToRefs(providersStore) @@ -176,11 +180,6 @@ watch([apiKey, baseUrl, accountId], () => { } }, { deep: true }) -// Actions -function handleSkip() { - emit('skipped') -} - async function handleSave() { if (!selectedProvider.value || !canSave.value) return @@ -200,9 +199,27 @@ async function handleSave() { ...config, } + await nextTick() emit('configured') } +function handlePreviousStep() { + if (step.value > 1) { + direction.value = 'previous' + step.value-- + } +} + +function handleNextStep() { + if (step.value < 3) { + direction.value = 'next' + step.value++ + } + else { + handleSave() + } +} + // Initialize with first popular provider onMounted(() => { if (popularProviders.value.length > 0) { @@ -213,112 +230,230 @@ onMounted(() => { + + diff --git a/packages/stage-ui/src/components/Widgets/Dialogs/Onboarding/OnboardingDialog.vue b/packages/stage-ui/src/components/Widgets/Dialogs/Onboarding/OnboardingDialog.vue index 15c7ca46f..f0ada04e1 100644 --- a/packages/stage-ui/src/components/Widgets/Dialogs/Onboarding/OnboardingDialog.vue +++ b/packages/stage-ui/src/components/Widgets/Dialogs/Onboarding/OnboardingDialog.vue @@ -3,7 +3,6 @@ import { useMediaQuery } from '@vueuse/core' import { DialogContent, DialogOverlay, DialogPortal, DialogRoot } from 'reka-ui' import { DrawerContent, DrawerHandle, DrawerOverlay, DrawerPortal, DrawerRoot } from 'vaul-vue' -import MobileOnboarding from './MobileOnboarding.vue' import Onboarding from './Onboarding.vue' const emit = defineEmits<{ @@ -20,7 +19,7 @@ const isDesktop = useMediaQuery('(min-width: 768px)') - + @@ -28,11 +27,9 @@ const isDesktop = useMediaQuery('(min-width: 768px)') - -
- - -
+ + +
diff --git a/packages/stage-ui/src/components/Widgets/Dialogs/Onboarding/index.ts b/packages/stage-ui/src/components/Widgets/Dialogs/Onboarding/index.ts index a1c09939d..8775304b7 100644 --- a/packages/stage-ui/src/components/Widgets/Dialogs/Onboarding/index.ts +++ b/packages/stage-ui/src/components/Widgets/Dialogs/Onboarding/index.ts @@ -1 +1,2 @@ +export { default as OnboardingScreen } from './Onboarding.vue' export { default as OnboardingDialog } from './OnboardingDialog.vue' diff --git a/packages/stage-ui/src/stores/onboarding.ts b/packages/stage-ui/src/stores/onboarding.ts index 15c2a443a..a53bb889f 100644 --- a/packages/stage-ui/src/stores/onboarding.ts +++ b/packages/stage-ui/src/stores/onboarding.ts @@ -77,6 +77,7 @@ export const useOnboardingStore = defineStore('onboarding', () => { shouldShowSetup, hasEssentialProviderConfigured, needsOnboarding, + initializeSetupCheck, markSetupCompleted, markSetupSkipped,