Newer
Older
navi-1 / webclient / src / components / chat / ShareChatModal.vue
<template>
  <GnModal
    :open="open"
    :title="t('share.title')"
    @update:open="emit('update:open', $event)"
  >
    <div class="share-body">
      <p class="share-intro">{{ t('share.intro') }}</p>

      <div v-if="loading" class="share-loading">
        <i class="ph ph-circle-notch share-spin"></i>
        {{ t('share.creating') }}
      </div>

      <p v-else-if="error" class="share-error">
        <i class="ph ph-warning-circle"></i>
        {{ error }}
      </p>

      <template v-else-if="url">
        <label class="share-label" for="share-url">{{ t('share.linkLabel') }}</label>
        <div class="share-link-row">
          <input
            id="share-url"
            class="share-url"
            type="text"
            readonly
            :value="url"
            @focus="$event.target.select()"
          />
          <GnCopyButton :text="url" :label="t('common.copy')" />
        </div>

        <p class="share-warning">
          <i class="ph ph-warning"></i>
          {{ t('share.warning') }}
        </p>
        <p class="share-hint">{{ t('share.killHint') }}</p>
      </template>

      <p v-else class="share-hint">{{ t('share.notYet') }}</p>
    </div>

    <template #actions>
      <GnButton variant="secondary" @click="emit('update:open', false)">
        {{ t('common.close') }}
      </GnButton>
      <GnButton
        variant="primary"
        icon="ph-link"
        :loading="loading"
        :disabled="!sessionId"
        @click="createLink"
      >
        {{ url ? t('share.regenerate') : t('share.create') }}
      </GnButton>
    </template>
  </GnModal>
</template>

<script setup>
import { ref, watch } from 'vue'
import { GnButton, GnCopyButton, GnModal } from 'gnexus-ui-kit/vue'
import { createChatShare } from '@/api/index.js'
import { useChatStore } from '@/stores/chat.js'
import { t } from '@/i18n/index.js'

const props = defineProps({
  open: Boolean,
})

const emit = defineEmits(['update:open'])

const chat = useChatStore()

const sessionId = ref(null)
const url = ref('')
const loading = ref(false)
const error = ref('')

// A new dialog is about the session open right then: a link kept from the
// previous one would name a different chat.
watch(
  () => props.open,
  open => {
    if (!open) return
    sessionId.value = chat.currentId
    url.value = ''
    error.value = ''
    loading.value = false
  },
  { immediate: true }
)

async function createLink() {
  const id = sessionId.value
  if (!id || loading.value) return
  loading.value = true
  error.value = ''
  try {
    const data = await createChatShare(id)
    url.value = data.url
  } catch (e) {
    // 409 = the session has no saved messages yet, so there is nothing to
    // publish; anything else is an unexpected failure.
    error.value = String(e?.message || '').includes('409')
      ? t('share.emptyChat')
      : t('share.createFailed')
    console.error('[share] create failed', e)
  } finally {
    loading.value = false
  }
}
</script>

<style scoped>
.share-body {
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: min(420px, calc(100vw - 48px));
}

.share-intro {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--gn-color-text-medium);
}

.share-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--gn-color-text-light);
}

.share-link-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.share-url {
  flex: 1;
  min-width: 0;
  padding: 8px 10px;
  border: 1px solid var(--gn-border-color-muted);
  border-radius: var(--gn-radius-md, 8px);
  background: var(--gn-surface-panel-muted, transparent);
  color: var(--gn-color-text-dark);
  font-family: var(--gn-font-mono, monospace);
  font-size: 12px;
}

.share-warning {
  display: flex;
  gap: 8px;
  margin: 0;
  padding: 10px 12px;
  border: 1px solid color-mix(in srgb, var(--gn-color-warning) 40%, transparent);
  border-radius: var(--gn-radius-md, 8px);
  background: color-mix(in srgb, var(--gn-color-warning) 8%, transparent);
  font-size: 12px;
  line-height: 1.5;
  color: var(--gn-color-text-medium);
}

.share-warning > i {
  flex: none;
  margin-top: 1px;
  color: var(--gn-color-warning);
}

.share-hint {
  margin: 0;
  font-size: 12px;
  color: var(--gn-color-text-light);
}

.share-loading {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 0;
  font-size: 13px;
  color: var(--gn-color-text-medium);
}

.share-spin {
  animation: share-spin 1s linear infinite;
}

@keyframes share-spin {
  to { transform: rotate(360deg); }
}

.share-error {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  font-size: 13px;
  color: var(--gn-color-error);
}
</style>