97 lines
4.4 KiB
Vue
97 lines
4.4 KiB
Vue
<script setup lang="ts">
|
||
import { computed, ref, watch } from 'vue'
|
||
import type { KbxAiAnswer, KbxAiScreenContext, KbxHelpContent, KbxSuggestionContext, KbxSuggestionRequest } from '@kbx/contracts'
|
||
import KbxHelpPanel from './KbxHelpPanel.vue'
|
||
import KbxAiAssistant from './KbxAiAssistant.vue'
|
||
import KbxUserSuggestionPanel from './KbxUserSuggestionPanel.vue'
|
||
|
||
type UtilityTab = 'help' | 'ai' | 'suggestion'
|
||
|
||
const props = defineProps<{
|
||
help?: KbxHelpContent
|
||
aiContext?: KbxAiScreenContext
|
||
aiAnswer?: KbxAiAnswer | null
|
||
aiLoading?: boolean
|
||
aiError?: string
|
||
aiCurrentScreenLabel?: string
|
||
aiQuickQuestions?: string[]
|
||
suggestionContext?: KbxSuggestionContext
|
||
suggestionSubmitting?: boolean
|
||
openTab?: UtilityTab | null
|
||
triggerless?: boolean
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
aiAsk: [question: string]
|
||
aiAction: [actionId: string]
|
||
aiOpenProposal: []
|
||
suggestionSubmit: [request: KbxSuggestionRequest]
|
||
close: []
|
||
}>()
|
||
|
||
const open = ref(false)
|
||
const active = ref<UtilityTab>('help')
|
||
const availableTabs = computed(() => [
|
||
props.help && 'help',
|
||
props.aiContext && 'ai',
|
||
props.suggestionContext && 'suggestion',
|
||
].filter(Boolean) as UtilityTab[])
|
||
|
||
function show(tab: UtilityTab) { active.value = tab; open.value = true }
|
||
function close(){ open.value=false; emit('close') }
|
||
watch(() => props.openTab, tab => { if(tab)show(tab); else if(props.triggerless)open.value=false }, { immediate:true })
|
||
</script>
|
||
|
||
<template>
|
||
<div class="kbx-utility-rail">
|
||
<nav v-if="!triggerless" class="kbx-utility-rail__buttons" aria-label="화면 보조기능">
|
||
<button v-if="help" type="button" @click="show('help')">도움말</button>
|
||
<button v-if="aiContext" type="button" @click="show('ai')">AI</button>
|
||
<button v-if="suggestionContext" type="button" @click="show('suggestion')">제안</button>
|
||
</nav>
|
||
|
||
<div v-if="open" class="kbx-utility-rail__backdrop" @click.self="close">
|
||
<aside class="kbx-utility-rail__panel" role="dialog" aria-modal="false" aria-label="화면 보조기능">
|
||
<header class="kbx-utility-rail__header">
|
||
<nav>
|
||
<button v-for="tab in availableTabs" :key="tab" type="button" :aria-current="active === tab ? 'page' : undefined" @click="active = tab">
|
||
{{ tab === 'help' ? '도움말' : tab === 'ai' ? 'AI' : '제안' }}
|
||
</button>
|
||
</nav>
|
||
<button type="button" aria-label="닫기" @click="close">×</button>
|
||
</header>
|
||
|
||
<KbxHelpPanel v-if="active === 'help' && help" :content="help" />
|
||
<KbxAiAssistant
|
||
v-else-if="active === 'ai' && aiContext"
|
||
:context="aiContext"
|
||
:answer="aiAnswer"
|
||
:loading="aiLoading"
|
||
:error="aiError"
|
||
:current-screen-label="aiCurrentScreenLabel"
|
||
:quick-questions="aiQuickQuestions"
|
||
@ask="emit('aiAsk', $event)"
|
||
@action="emit('aiAction', $event)"
|
||
@open-proposal="emit('aiOpenProposal')"
|
||
/>
|
||
<KbxUserSuggestionPanel
|
||
v-else-if="active === 'suggestion' && suggestionContext"
|
||
:context="suggestionContext"
|
||
:submitting="suggestionSubmitting"
|
||
@submit="emit('suggestionSubmit', $event)"
|
||
/>
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.kbx-utility-rail__buttons{display:flex;align-items:center;gap:var(--kbx-space-1)}
|
||
.kbx-utility-rail__buttons button,.kbx-utility-rail__header button{min-height:var(--kbx-control-sm);padding:0 var(--kbx-space-2);border:var(--kbx-border-width) solid var(--kbx-color-border);border-radius:var(--kbx-radius-sm);background:var(--kbx-color-surface);color:var(--kbx-color-text)}
|
||
.kbx-utility-rail__backdrop{position:fixed;inset:0;z-index:1000;background:var(--kbx-color-overlay);display:flex;justify-content:flex-end}
|
||
.kbx-utility-rail__panel{width:min(var(--kbx-utility-panel-width),94vw);height:100%;overflow:auto;background:var(--kbx-color-surface);border-left:var(--kbx-border-width) solid var(--kbx-color-border);box-shadow:var(--kbx-shadow-overlay)}
|
||
.kbx-utility-rail__header{position:sticky;top:0;z-index:2;display:flex;justify-content:space-between;gap:var(--kbx-space-2);padding:var(--kbx-space-2);border-bottom:var(--kbx-border-width) solid var(--kbx-color-border);background:var(--kbx-color-surface)}
|
||
.kbx-utility-rail__header nav{display:flex;gap:var(--kbx-space-1)}
|
||
.kbx-utility-rail__header [aria-current="page"]{border-color:var(--kbx-color-primary);color:var(--kbx-color-primary);font-weight:600}
|
||
</style>
|