@@ -4,6 +4,7 @@ import { updateAMAConfigSchema } from '@chatsift/api/ama-schemas';
44import { amaQuestionsChannel } from '@chatsift/core' ;
55import { useQueryClient } from '@tanstack/react-query' ;
66import { ChannelType } from 'discord-api-types/v10' ;
7+ import Link from 'next/link' ;
78import { useParams , useRouter } from 'next/navigation' ;
89import { useState } from 'react' ;
910import { NormalPromptFields } from '../../_components/NormalPromptFields' ;
@@ -12,6 +13,7 @@ import { PromptModeToggle } from '../../_components/PromptModeToggle';
1213import { PromptPreview } from '../../_components/PromptPreview' ;
1314import { AuthorAvatar } from '../questions/_components/AuthorAvatar' ;
1415import { userLabel } from '../questions/_components/userLabel' ;
16+ import { StatChip } from './StatChip' ;
1517import { QUESTION_STATE_TILES , valenceClass } from './questionStateTiles' ;
1618import { APIError } from '@/api/error' ;
1719import type { AMAStats , PossiblyMissingChannelInfo , UpdateAMABody } from '@/api/routes/ama' ;
@@ -573,6 +575,83 @@ export function AMADetails() {
573575 </ p >
574576 ) }
575577
578+ { /* Analytics & Export Card -- sits directly under the Question Triage banner rather than down with
579+ the config cards: it's the "how is this AMA doing" answer someone opens this page for, and its tag
580+ chips are a second entry point into Triage, so it belongs next to the primary one. */ }
581+ < div className = "rounded-lg border border-on-secondary bg-card p-6 dark:border-on-secondary-dark dark:bg-card-dark lg:col-span-2" >
582+ < div className = "mb-4 flex items-center justify-between" >
583+ < h2 className = "text-xl font-medium text-primary dark:text-primary-dark" > Analytics & Export</ h2 >
584+ { canManage && (
585+ < Button
586+ className = "px-3 py-1.5 text-sm bg-on-tertiary dark:bg-on-tertiary-dark text-primary dark:text-primary-dark rounded-md hover:bg-on-secondary dark:hover:bg-on-secondary-dark transition-colors disabled:opacity-50"
587+ isDisabled = { exportQuestions . isPending }
588+ onPress = { handleExport }
589+ type = "button"
590+ >
591+ { exportQuestions . isPending ? 'Exporting…' : 'Export CSV' }
592+ </ Button >
593+ ) }
594+ </ div >
595+
596+ { isStatsLoading ? (
597+ < Skeleton className = "h-24 w-full" />
598+ ) : stats ? (
599+ < div className = "flex flex-col gap-6" >
600+ < div className = "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-7" >
601+ < div className = "rounded-lg border border-on-secondary p-4 text-center dark:border-on-secondary-dark" >
602+ < p className = "text-2xl font-semibold text-primary dark:text-primary-dark" > { stats . total } </ p >
603+ < p className = "mt-1 text-xs text-secondary dark:text-secondary-dark" > Total Questions</ p >
604+ </ div >
605+ < div className = "rounded-lg border border-on-secondary p-4 text-center dark:border-on-secondary-dark" >
606+ < p className = "text-2xl font-semibold text-primary dark:text-primary-dark" > { stats . uniqueAskerCount } </ p >
607+ < p className = "mt-1 text-xs text-secondary dark:text-secondary-dark" > Unique Askers</ p >
608+ </ div >
609+ { /* "Duplicates", not "Merged Duplicates" -- at lg:grid-cols-7 the longer label is the only one
610+ that wraps to two lines, and a grid row stretches every other tile to match it. */ }
611+ < div className = "rounded-lg border border-on-secondary p-4 text-center dark:border-on-secondary-dark" >
612+ < p className = "text-2xl font-semibold text-primary dark:text-primary-dark" >
613+ { stats . mergedDuplicatesCount }
614+ </ p >
615+ < p className = "mt-1 text-xs text-secondary dark:text-secondary-dark" > Duplicates</ p >
616+ </ div >
617+ { QUESTION_STATE_TILES . map ( ( { state, label, valence } ) => (
618+ < div
619+ className = "rounded-lg border border-on-secondary p-4 text-center dark:border-on-secondary-dark"
620+ key = { state }
621+ >
622+ < p className = { `text-2xl font-semibold ${ valenceClass [ valence ] } ` } >
623+ { stats . byState [ state as keyof AMAStats [ 'byState' ] ] }
624+ </ p >
625+ < p className = "mt-1 text-xs text-secondary dark:text-secondary-dark" > { label } </ p >
626+ </ div >
627+ ) ) }
628+ </ div >
629+
630+ { /* Chips rather than more tiles: tag count is unbounded, so a grid row would blow the card up
631+ once a session has more than a handful. Each one deep-links into Triage pre-filtered to that
632+ tag -- `?tag=` is exactly the param `useTagFilter` reads (QuestionTagFilter.tsx). */ }
633+ { stats . byTag . length > 0 && (
634+ < div >
635+ < h3 className = "mb-2 text-sm font-medium text-primary dark:text-primary-dark" > Questions by Tag</ h3 >
636+ < div className = "flex flex-wrap gap-2" >
637+ { stats . byTag . map ( ( tag ) => (
638+ < Link
639+ className = "rounded-md transition-opacity hover:opacity-80"
640+ href = { `/dashboard/${ params . id } /ama/amas/${ params . amaId } /questions?tag=${ tag . id } ` }
641+ key = { tag . id }
642+ >
643+ < StatChip label = { tag . name } value = { tag . count } />
644+ </ Link >
645+ ) ) }
646+ </ div >
647+ </ div >
648+ ) }
649+ </ div >
650+ ) : (
651+ < p className = "text-sm text-secondary dark:text-secondary-dark" > Unable to load question stats.</ p >
652+ ) }
653+ </ div >
654+
576655 { /* Session Information Card */ }
577656 < div className = "rounded-lg border border-on-secondary bg-card p-6 dark:border-on-secondary-dark dark:bg-card-dark" >
578657 < div className = "mb-4 flex items-center justify-between" >
@@ -1021,53 +1100,6 @@ export function AMADetails() {
10211100 </ div >
10221101 </ div >
10231102
1024- { /* Analytics & Export Card */ }
1025- < div className = "rounded-lg border border-on-secondary bg-card p-6 dark:border-on-secondary-dark dark:bg-card-dark lg:col-span-2" >
1026- < div className = "mb-4 flex items-center justify-between" >
1027- < h2 className = "text-xl font-medium text-primary dark:text-primary-dark" > Analytics & Export</ h2 >
1028- { canManage && (
1029- < Button
1030- className = "px-3 py-1.5 text-sm bg-on-tertiary dark:bg-on-tertiary-dark text-primary dark:text-primary-dark rounded-md hover:bg-on-secondary dark:hover:bg-on-secondary-dark transition-colors disabled:opacity-50"
1031- isDisabled = { exportQuestions . isPending }
1032- onPress = { handleExport }
1033- type = "button"
1034- >
1035- { exportQuestions . isPending ? 'Exporting…' : 'Export CSV' }
1036- </ Button >
1037- ) }
1038- </ div >
1039-
1040- { isStatsLoading ? (
1041- < Skeleton className = "h-24 w-full" />
1042- ) : stats ? (
1043- < div className = "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6" >
1044- < div className = "rounded-lg border border-on-secondary p-4 text-center dark:border-on-secondary-dark" >
1045- < p className = "text-2xl font-semibold text-primary dark:text-primary-dark" > { stats . total } </ p >
1046- < p className = "mt-1 text-xs text-secondary dark:text-secondary-dark" > Total Questions</ p >
1047- </ div >
1048- < div className = "rounded-lg border border-on-secondary p-4 text-center dark:border-on-secondary-dark" >
1049- < p className = "text-2xl font-semibold text-primary dark:text-primary-dark" >
1050- { stats . mergedDuplicatesCount }
1051- </ p >
1052- < p className = "mt-1 text-xs text-secondary dark:text-secondary-dark" > Merged Duplicates</ p >
1053- </ div >
1054- { QUESTION_STATE_TILES . map ( ( { state, label, valence } ) => (
1055- < div
1056- className = "rounded-lg border border-on-secondary p-4 text-center dark:border-on-secondary-dark"
1057- key = { state }
1058- >
1059- < p className = { `text-2xl font-semibold ${ valenceClass [ valence ] } ` } >
1060- { stats . byState [ state as keyof AMAStats [ 'byState' ] ] }
1061- </ p >
1062- < p className = "mt-1 text-xs text-secondary dark:text-secondary-dark" > { label } </ p >
1063- </ div >
1064- ) ) }
1065- </ div >
1066- ) : (
1067- < p className = "text-sm text-secondary dark:text-secondary-dark" > Unable to load question stats.</ p >
1068- ) }
1069- </ div >
1070-
10711103 { /* Question Submissions Card */ }
10721104 { canManage && (
10731105 < div className = "rounded-lg border border-on-secondary bg-card p-6 dark:border-on-secondary-dark dark:bg-card-dark lg:col-span-2" >
0 commit comments