Skip to content

Commit b081c04

Browse files
committed
feat: usage of component di poc
1 parent 89e4e62 commit b081c04

3 files changed

Lines changed: 220 additions & 0 deletions

File tree

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { useEffect, useRef, useState } from "react";
2+
3+
import styled from "styled-components";
4+
import { useViewState } from "terriajs-plugin-api";
5+
import { ExplorerWindowComponentsDI } from "terriajs/lib/ReactViews/ExplorerWindow/ExplorerWindowComponentsDI";
6+
import SearchBox from "terriajs/lib/ReactViews/Search/SearchBox";
7+
import Box from "terriajs/lib/Styled/Box";
8+
import Icon from "terriajs/lib/Styled/Icon";
9+
10+
const AI_SEARCH_EXAMPLES = [
11+
"Show me forestry data from 2010",
12+
"What data is available for Perth?",
13+
"Flood risk layers near Brisbane",
14+
"Imagery from the last 5 years",
15+
"2010 to 2015 ABS data"
16+
];
17+
18+
function useTypewriterPlaceholder(active: boolean): string {
19+
const [placeholder, setPlaceholder] = useState("");
20+
const exampleIndex = useRef(0);
21+
const charIndex = useRef(0);
22+
const deleting = useRef(false);
23+
const timer = useRef<ReturnType<typeof setTimeout>>();
24+
25+
useEffect(() => {
26+
if (!active) {
27+
setPlaceholder("");
28+
exampleIndex.current = 0;
29+
charIndex.current = 0;
30+
deleting.current = false;
31+
return;
32+
}
33+
34+
function tick() {
35+
const current = AI_SEARCH_EXAMPLES[exampleIndex.current];
36+
37+
if (!deleting.current) {
38+
charIndex.current += 1;
39+
setPlaceholder(current.slice(0, charIndex.current));
40+
if (charIndex.current === current.length) {
41+
// pause at end before deleting
42+
timer.current = setTimeout(() => {
43+
deleting.current = true;
44+
tick();
45+
}, 1800);
46+
return;
47+
}
48+
} else {
49+
charIndex.current -= 1;
50+
setPlaceholder(current.slice(0, charIndex.current));
51+
if (charIndex.current === 0) {
52+
deleting.current = false;
53+
exampleIndex.current =
54+
(exampleIndex.current + 1) % AI_SEARCH_EXAMPLES.length;
55+
// pause before typing next
56+
timer.current = setTimeout(tick, 400);
57+
return;
58+
}
59+
}
60+
61+
timer.current = setTimeout(tick, deleting.current ? 25 : 50);
62+
}
63+
64+
timer.current = setTimeout(tick, 300);
65+
return () => clearTimeout(timer.current);
66+
}, [active]);
67+
68+
return placeholder;
69+
}
70+
71+
const TooltipWrapper = styled.span`
72+
position: relative;
73+
display: inline-flex;
74+
align-items: center;
75+
color: ${(p) => p.theme.textDark};
76+
opacity: 0.5;
77+
cursor: help;
78+
&:hover::after {
79+
content: attr(data-tooltip);
80+
position: absolute;
81+
top: 50%;
82+
left: calc(100% + 6px);
83+
transform: translateY(-50%);
84+
background: ${(p) => p.theme.dark};
85+
color: white;
86+
font-size: 10px;
87+
line-height: 1.4;
88+
padding: 4px 7px;
89+
border-radius: 4px;
90+
white-space: normal;
91+
width: 140px;
92+
pointer-events: none;
93+
z-index: 100;
94+
}
95+
`;
96+
97+
const SearchBoxWrapper = styled.div<{ aiSearch: boolean }>`
98+
${(p) =>
99+
p.aiSearch &&
100+
`
101+
input {
102+
outline: 2px solid ${p.theme.colorPrimary};
103+
outline-offset: -2px;
104+
}
105+
`}
106+
`;
107+
108+
const ToggleLabel = styled.label`
109+
display: flex;
110+
align-items: center;
111+
gap: 6px;
112+
cursor: pointer;
113+
font-size: 12px;
114+
color: ${(p) => p.theme.textDark};
115+
user-select: none;
116+
`;
117+
118+
const ToggleInput = styled.input.attrs({ type: "checkbox" })`
119+
appearance: none;
120+
width: 28px;
121+
height: 16px;
122+
background: ${(p) => p.theme.greyLighter};
123+
border-radius: 8px;
124+
position: relative;
125+
cursor: pointer;
126+
transition: background 0.2s;
127+
flex-shrink: 0;
128+
&:checked {
129+
background: ${(p) => p.theme.colorPrimary};
130+
}
131+
&::after {
132+
content: "";
133+
position: absolute;
134+
width: 12px;
135+
height: 12px;
136+
border-radius: 50%;
137+
background: white;
138+
top: 2px;
139+
left: 2px;
140+
transition: left 0.2s;
141+
}
142+
&:checked::after {
143+
left: 14px;
144+
}
145+
`;
146+
147+
export const MapAiCatalogSearch = ({
148+
searchPlaceholder,
149+
searchText,
150+
onSearchTextChanged,
151+
onDoSearch
152+
}: React.ComponentProps<
153+
typeof ExplorerWindowComponentsDI.DataCatalogSearch
154+
>) => {
155+
const [useAiSearch, setUseAiSearch] = useState(false);
156+
const viewState = useViewState();
157+
const { searchState } = viewState;
158+
159+
const aiPlaceholder = useTypewriterPlaceholder(useAiSearch);
160+
161+
return (
162+
<Box column>
163+
<SearchBoxWrapper aiSearch={useAiSearch}>
164+
<SearchBox
165+
searchText={searchText}
166+
onSearchTextChanged={onSearchTextChanged}
167+
onDoSearch={onDoSearch}
168+
placeholder={useAiSearch ? aiPlaceholder : searchPlaceholder}
169+
// iconGlyph={ searchState.useAiSearch ? Icon.GLYPHS.sparkle : undefined }
170+
/>
171+
</SearchBoxWrapper>
172+
<Box
173+
fullWidth
174+
css={`
175+
padding: 2px 10px;
176+
`}
177+
>
178+
<ToggleLabel>
179+
<ToggleInput
180+
checked={useAiSearch}
181+
onChange={() => {
182+
setUseAiSearch((old) => !old);
183+
const provider = searchState.catalogSearchProvider as any;
184+
if ("useAiSearch" in provider) {
185+
provider.useAiSearch = !provider.useAiSearch;
186+
}
187+
}}
188+
/>
189+
Conversational AI Search
190+
<TooltipWrapper data-tooltip="(experimental) Use natural language to find data.">
191+
<Icon glyph={Icon.GLYPHS.help} css="width: 12px; height: 12px;" />
192+
</TooltipWrapper>
193+
</ToggleLabel>
194+
</Box>
195+
</Box>
196+
);
197+
};

lib/Views/render.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import Variables from "../Styles/variables.scss";
66
import "./global.scss";
77
import { Loader } from "./Loader";
88
import { terriaStore } from "./terriaStore";
9+
import { ExplorerWindowComponentsDI } from "terriajs/lib/ReactViews/ExplorerWindow/ExplorerWindowComponentsDI";
10+
import { MapAiCatalogSearch } from "./DataCatalog/MapAiCatalogSearch";
11+
12+
ExplorerWindowComponentsDI.DataCatalogSearch = observer(MapAiCatalogSearch);
913

1014
// Lazy load the entire TerriaUserInterface component
1115
const LazyTerriaUserInterface = React.lazy(() =>

types/styled-components.d.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Pull in the css prop type addition to react attributes
2+
/// <reference types="styled-components/cssprop" />
3+
4+
import { CSSProp } from "styled-components";
5+
import { terriaTheme } from "terriajs/lib/ReactViews/StandardUserInterface/StandardTheme";
6+
7+
type Theme = typeof terriaTheme;
8+
9+
declare module "styled-components" {
10+
export interface DefaultTheme extends Theme {}
11+
}
12+
// See https://github.com/styled-components/styled-components/issues/2528
13+
// .css isn't included in @types/styled-components because it has to be enabled
14+
// through a babel plugin or macro
15+
declare module "react" {
16+
interface Attributes {
17+
css?: CSSProp;
18+
}
19+
}

0 commit comments

Comments
 (0)