Skip to content

Commit 241e10a

Browse files
committed
add clusters filter breadcrumbs
1 parent 994d1e3 commit 241e10a

File tree

8 files changed

+157
-22
lines changed

8 files changed

+157
-22
lines changed

scatter/report/components/BroadlisteningGuide.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import {
55
DialogHeader, DialogRoot,
66
DialogTrigger
77
} from '@/components/ui/dialog'
8-
import {Button, Heading, Text} from '@chakra-ui/react'
8+
import {Button, Heading, Image, Text} from '@chakra-ui/react'
99
import {CircleHelpIcon} from 'lucide-react'
1010

1111
export function BroadlisteningGuide() {
1212
return (
13-
<DialogRoot size="lg" placement="center" motionPreset="slide-in-bottom">
13+
<DialogRoot size="xl" placement="center" motionPreset="slide-in-bottom">
1414
<DialogTrigger asChild>
1515
<Button variant={'outline'}>
1616
<CircleHelpIcon />
@@ -19,11 +19,17 @@ export function BroadlisteningGuide() {
1919
</DialogTrigger>
2020
<DialogContent>
2121
<DialogHeader>
22-
<Heading as={'h2'} size={'xl'} mb={2} className={'headingColor'}>ブロードリスニングとは?</Heading>
22+
<Heading as={'h2'} size={'xl'} className={'headingColor'}>ブロードリスニングとは?</Heading>
2323
<DialogCloseTrigger />
2424
</DialogHeader>
2525
<DialogBody>
26-
ブロードリスニングとは「広く声を収集し、収集した声をAI技術で分析・可視化する手法」です。
26+
<Text mb={4} fontSize={'md'}>
27+
ブロードリスニングとは<b>「広く声を収集し、収集した声をAI技術で分析・可視化する手法」</b>です。
28+
</Text>
29+
<Image mb={4} src={'/broadlistening.png'} alt={'ブロードリスニングのイメージ'} />
30+
<Text>
31+
かつてラジオやテレビなど放送技術の発展により、大勢の人に声を届けることが可能になりました。しかし大勢の声を聞くことはできませんでした。2023年ごろから、大規模言語モデルの技術の発展により、大勢の意見を要約し、わかりやすく可視化したりレポートにまとめたりすることが可能になりました。この「大勢の声を聞く技術」のことをブロードリスニングと言います。
32+
</Text>
2733
</DialogBody>
2834
</DialogContent>
2935
</DialogRoot>

scatter/report/components/ClientContainer.tsx

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {LoadingBar} from '@/components/LoadingBar'
1010
import {FilterSettingDialog} from '@/components/FilterSettingDialog'
1111
import {ClusterOverview} from '@/components/ClusterOverview'
1212
import {SelectChartButton} from '@/components/charts/SelectChartButton'
13+
import {ClusterBreadcrumb} from '@/components/ClusterBreadcrumb'
1314

1415
type Props = {
1516
resultSize: number
@@ -28,6 +29,7 @@ export function ClientContainer({resultSize, meta, children}: PropsWithChildren<
2829
const [result, setResult] = useState<Result>()
2930
const [rootLevel, setRootLevel] = useState(0)
3031
const [filteredResult, setFilteredResult] = useState<Result>()
32+
const [selectedClusters, setSelectedClusters] = useState<Cluster[]>([])
3133
const [openFilterSetting, setOpenFilterSetting] = useState(false)
3234
const [selectedChart, setSelectedChart] = useState('scatter')
3335

@@ -37,11 +39,11 @@ export function ClientContainer({resultSize, meta, children}: PropsWithChildren<
3739

3840
function onChangeFilter(lv1: string, lv2: string, lv3: string, lv4: string) {
3941
if (!result) return
40-
const filteredClusters = getFilteredClusters(result.clusters || [], lv1, lv2, lv3, lv4)
4142
setRootLevel(getRootLevel(lv1, lv2, lv3, lv4))
43+
setSelectedClusters(getSelectedClusters(result.clusters || [], lv1, lv2, lv3, lv4))
4244
setFilteredResult({
4345
...result,
44-
clusters: filteredClusters
46+
clusters: getFilteredClusters(result.clusters || [], lv1, lv2, lv3, lv4)
4547
})
4648
}
4749

@@ -83,12 +85,14 @@ export function ClientContainer({resultSize, meta, children}: PropsWithChildren<
8385
}
8486
return (
8587
<>
86-
<FilterSettingDialog
87-
result={result}
88-
isOpen={openFilterSetting}
89-
onClose={() => {setOpenFilterSetting(false)}}
90-
onChangeFilter={onChangeFilter}
91-
/>
88+
{openFilterSetting && (
89+
<FilterSettingDialog
90+
result={result}
91+
selectedClusters={selectedClusters}
92+
onClose={() => {setOpenFilterSetting(false)}}
93+
onChangeFilter={onChangeFilter}
94+
/>
95+
)}
9296
<Chart
9397
result={filteredResult}
9498
rootLevel={rootLevel}
@@ -100,6 +104,10 @@ export function ClientContainer({resultSize, meta, children}: PropsWithChildren<
100104
onClickSetting={() => {setOpenFilterSetting(true)}}
101105
isApplyFilter={result.clusters.length !== filteredResult.clusters.length}
102106
/>
107+
<ClusterBreadcrumb
108+
selectedClusters={selectedClusters}
109+
onChangeFilter={onChangeFilter}
110+
/>
103111
{ rootLevel === 0 && children }
104112
{ rootLevel !== 0 && (
105113
filteredResult.clusters.filter(c => c.level === rootLevel + 1).map(c => (
@@ -120,6 +128,15 @@ function getRootLevel(level1Id:string, level2Id:string, level3Id:string, level4I
120128
return 0
121129
}
122130

131+
function getSelectedClusters(clusters: Cluster[], level1Id:string, level2Id:string, level3Id:string, level4Id:string): Cluster[] {
132+
const results: Cluster[] = []
133+
if (level1Id !== '0') results.push(clusters.find(c => c.id === level1Id)!)
134+
if (level2Id !== '0') results.push(clusters.find(c => c.id === level2Id)!)
135+
if (level3Id !== '0') results.push(clusters.find(c => c.id === level3Id)!)
136+
if (level4Id !== '0') results.push(clusters.find(c => c.id === level4Id)!)
137+
return results
138+
}
139+
123140
function getFilteredClusters(clusters: Cluster[], level1Id:string, level2Id:string, level3Id:string, level4Id:string): Cluster[] {
124141
if (level4Id !== '0') {
125142
const lv1cluster = clusters.find(c => c.id === level1Id)!
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {BreadcrumbLink, BreadcrumbRoot} from '@/components/ui/breadcrumb'
2+
import {Box, Text} from '@chakra-ui/react'
3+
import {Cluster} from '@/type'
4+
5+
type Props = {
6+
selectedClusters: Cluster[]
7+
onChangeFilter: (level1: string, level2: string, level3: string, level4: string) => void
8+
}
9+
10+
export function ClusterBreadcrumb({selectedClusters, onChangeFilter}: Props) {
11+
12+
function onChange(level: number) {
13+
onChangeFilter('0', '0', '0', '0')
14+
switch (level) {
15+
case 0:
16+
onChangeFilter('0', '0', '0', '0')
17+
break
18+
case 1:
19+
onChangeFilter(selectedClusters[0].id, '0', '0', '0')
20+
break
21+
case 2:
22+
onChangeFilter(selectedClusters[0].id, selectedClusters[1].id, '0', '0')
23+
break
24+
case 3:
25+
onChangeFilter(
26+
selectedClusters[0].id,
27+
selectedClusters[1].id,
28+
selectedClusters[2].id,
29+
'0'
30+
)
31+
break
32+
case 4:
33+
// do nothing.
34+
break
35+
}
36+
}
37+
38+
if (!selectedClusters.length) return <></>
39+
return (
40+
<Box mx={'auto'} maxW={'870px'} mb={6} display={{base: 'none', md: 'block'}}>
41+
<Text fontSize={'sm'} fontWeight={'bold'}>表示中のクラスター</Text>
42+
<BreadcrumbRoot
43+
size="lg"
44+
>
45+
<BreadcrumbLink
46+
fontSize={'sm'}
47+
w={'30px'}
48+
onClick={() => {onChange(0)}}
49+
cursor={'pointer'}
50+
>全て</BreadcrumbLink>
51+
{selectedClusters.map((cluster, i) => (
52+
<BreadcrumbLink
53+
key={i}
54+
fontSize={'sm'}
55+
fontWeight={i === selectedClusters.length - 1 ? 'bold' : 'normal'}
56+
onClick={() => {onChange(cluster.level)}}
57+
cursor={'pointer'}
58+
>{cluster.label}</BreadcrumbLink>
59+
))}
60+
</BreadcrumbRoot>
61+
</Box>
62+
)
63+
}

scatter/report/components/ClusterOverview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ type Props = {
88

99
export function ClusterOverview({cluster}: Props) {
1010
return (
11-
<Box mx={'auto'} maxW={'750px'} my={12}>
11+
<Box mx={'auto'} maxW={'750px'} mb={12}>
1212
<Box mb={2}>
1313
<Heading fontSize={'2xl'} className={'headingColor'} mb={1}>{cluster.label}</Heading>
1414
<Text fontWeight={'bold'}><Icon mr={1}><MessageSquareIcon size={20} /></Icon>{cluster.value}コメント</Text>

scatter/report/components/FilterSettingDialog.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {Box, Button, Text, VStack} from '@chakra-ui/react'
22
import React, {useState} from 'react'
33
import {NativeSelectField, NativeSelectRoot} from '@/components/ui/native-select'
4-
import {Result} from '@/type'
4+
import {Cluster, Result} from '@/type'
55
import {
66
DialogBody,
77
DialogCloseTrigger,
@@ -15,16 +15,16 @@ import {ChevronDownIcon} from 'lucide-react'
1515

1616
type Props = {
1717
result: Result
18-
isOpen: boolean
18+
selectedClusters: Cluster[]
1919
onClose: () => void
2020
onChangeFilter: (level1: string, level2: string, level3: string, level4: string) => void
2121
}
2222

23-
export function FilterSettingDialog({result, isOpen, onClose, onChangeFilter}: Props) {
24-
const [level1, setLevel1] = useState<string>('0')
25-
const [level2, setLevel2] = useState<string>('0')
26-
const [level3, setLevel3] = useState<string>('0')
27-
const [level4, setLevel4] = useState<string>('0')
23+
export function FilterSettingDialog({result, selectedClusters, onClose, onChangeFilter}: Props) {
24+
const [level1, setLevel1] = useState<string>(selectedClusters[0]?.id || '0')
25+
const [level2, setLevel2] = useState<string>(selectedClusters[1]?.id || '0')
26+
const [level3, setLevel3] = useState<string>(selectedClusters[2]?.id || '0')
27+
const [level4, setLevel4] = useState<string>(selectedClusters[3]?.id || '0')
2828

2929
function onChangeLevel(level: number, id: string) {
3030
switch (level) {
@@ -53,9 +53,17 @@ export function FilterSettingDialog({result, isOpen, onClose, onChangeFilter}: P
5353
onChangeFilter(level1, level2, level3, level4)
5454
onClose()
5555
}
56+
function onReset() {
57+
setLevel1('0')
58+
setLevel2('0')
59+
setLevel3('0')
60+
setLevel4('0')
61+
onChangeFilter('0', '0', '0', '0')
62+
onClose()
63+
}
5664

5765
return (
58-
<DialogRoot lazyMount open={isOpen} onOpenChange={onClose}>
66+
<DialogRoot lazyMount open={true} onOpenChange={onClose}>
5967
<DialogContent>
6068
<DialogHeader>
6169
<DialogTitle>表示クラスター設定</DialogTitle>
@@ -125,6 +133,7 @@ export function FilterSettingDialog({result, isOpen, onClose, onChangeFilter}: P
125133
)}
126134
</DialogBody>
127135
<DialogFooter>
136+
<Button variant={'outline'} onClick={onReset}>リセット</Button>
128137
<Button onClick={onApply}>設定を適用</Button>
129138
</DialogFooter>
130139
<DialogCloseTrigger />

scatter/report/components/charts/SelectChartButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type Props = {
1717

1818
export function SelectChartButton({selected, onChange, onClickSetting, isApplyFilter}: Props) {
1919
return (
20-
<HStack w={'100%'} justify={'center'} align={'center'}>
20+
<HStack w={'100%'} justify={'center'} align={'center'} mb={10}>
2121
<RadioCardRoot
2222
orientation="horizontal"
2323
align="center"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Breadcrumb, type SystemStyleObject } from '@chakra-ui/react'
2+
import * as React from 'react'
3+
4+
export interface BreadcrumbRootProps extends Breadcrumb.RootProps {
5+
separator?: React.ReactNode
6+
separatorGap?: SystemStyleObject['gap']
7+
}
8+
9+
export const BreadcrumbRoot = React.forwardRef<
10+
HTMLDivElement,
11+
BreadcrumbRootProps
12+
>(function BreadcrumbRoot(props, ref) {
13+
const { separator, separatorGap, children, ...rest } = props
14+
15+
const validChildren = React.Children.toArray(children).filter(
16+
React.isValidElement,
17+
)
18+
19+
return (
20+
<Breadcrumb.Root ref={ref} {...rest}>
21+
<Breadcrumb.List gap={separatorGap}>
22+
{validChildren.map((child, index) => {
23+
const last = index === validChildren.length - 1
24+
return (
25+
<React.Fragment key={index}>
26+
<Breadcrumb.Item>{child}</Breadcrumb.Item>
27+
{!last && (
28+
<Breadcrumb.Separator>{separator}</Breadcrumb.Separator>
29+
)}
30+
</React.Fragment>
31+
)
32+
})}
33+
</Breadcrumb.List>
34+
</Breadcrumb.Root>
35+
)
36+
})
37+
38+
export const BreadcrumbLink = Breadcrumb.Link
39+
export const BreadcrumbCurrentLink = Breadcrumb.CurrentLink
40+
export const BreadcrumbEllipsis = Breadcrumb.Ellipsis
1.15 MB
Loading

0 commit comments

Comments
 (0)