From 842a8670e78e5312b67d02e7d3fcb47a04f56b09 Mon Sep 17 00:00:00 2001 From: tnagorra Date: Sat, 30 May 2026 08:50:53 +0545 Subject: [PATCH 01/11] feat: move "Google Calendar" connection to sidepane --- src/App/index.tsx | 49 ++--- src/App/providers/GoogleCalendarProvider.tsx | 171 ++++++++++++++++++ src/contexts/googleCalendar.tsx | 55 ++++++ src/hooks/useGoogleCalendar.ts | 159 +--------------- src/views/DailyJournal/StartSidebar/index.tsx | 46 ++++- src/views/DailyJournal/index.tsx | 1 - src/views/Settings/index.tsx | 57 ------ 7 files changed, 293 insertions(+), 245 deletions(-) create mode 100644 src/App/providers/GoogleCalendarProvider.tsx create mode 100644 src/contexts/googleCalendar.tsx diff --git a/src/App/index.tsx b/src/App/index.tsx index 29ccf8f..61c28d1 100644 --- a/src/App/index.tsx +++ b/src/App/index.tsx @@ -21,6 +21,7 @@ import AuthProvider from './providers/AuthProvider'; import CommandProvider from './providers/CommandProvider'; import DateProvider from './providers/DateProvider'; import EnumsProvider from './providers/EnumsProvider'; +import GoogleCalendarProvider from './providers/GoogleCalendarProvider'; import LocalStorageProvider from './providers/LocalStorageProvider'; import NavbarProvider from './providers/NavbarProvider'; import SizeProvider from './providers/SizeProvider'; @@ -83,28 +84,32 @@ function App() { - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/providers/GoogleCalendarProvider.tsx b/src/App/providers/GoogleCalendarProvider.tsx new file mode 100644 index 0000000..c1d40c1 --- /dev/null +++ b/src/App/providers/GoogleCalendarProvider.tsx @@ -0,0 +1,171 @@ +import { + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; +import { + googleLogout, + useGoogleLogin, +} from '@react-oauth/google'; +import { useQueryClient } from '@tanstack/react-query'; + +import GoogleCalendarContext, { + type GoogleCalendarContextValue, + type GoogleCalendarEvent, +} from '#contexts/googleCalendar'; + +const STORAGE_KEY = 'timur-google-calendar-token'; +const CALENDAR_SCOPE = 'https://www.googleapis.com/auth/calendar.readonly'; +const CALENDAR_API_BASE = 'https://www.googleapis.com/calendar/v3'; +const GCAL_QUERY_KEY = 'googleCalendarEvents'; + +interface StoredToken { + accessToken: string; + expiresAt: number; +} + +function getStoredToken(): StoredToken | null { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return null; + return JSON.parse(raw) as StoredToken; + } catch { + return null; + } +} + +function isTokenValid(token: StoredToken | null): token is StoredToken { + if (!token) return false; + return Date.now() < token.expiresAt - 60_000; +} + +interface Props { + children: React.ReactNode; +} + +function GoogleCalendarProvider(props: Props) { + const { children } = props; + const queryClient = useQueryClient(); + const clientId = import.meta.env.APP_GOOGLE_OAUTH_CLIENT_ID as string | undefined; + + const [storedToken, setStoredToken] = useState(() => getStoredToken()); + + const isConnected = isTokenValid(storedToken); + + useEffect(() => { + if (!storedToken) return undefined; + const msUntilExpiry = storedToken.expiresAt - Date.now(); + if (msUntilExpiry <= 0) { + setStoredToken(null); + return undefined; + } + const timeout = window.setTimeout(() => setStoredToken(null), msUntilExpiry); + return () => window.clearTimeout(timeout); + }, [storedToken]); + + const saveToken = useCallback((accessToken: string, expiresIn: number) => { + const token: StoredToken = { + accessToken, + expiresAt: Date.now() + expiresIn * 1000, + }; + localStorage.setItem(STORAGE_KEY, JSON.stringify(token)); + setStoredToken(token); + }, []); + + const clearToken = useCallback(() => { + localStorage.removeItem(STORAGE_KEY); + setStoredToken(null); + }, []); + + const login = useGoogleLogin({ + scope: CALENDAR_SCOPE, + onSuccess: (response) => { + saveToken(response.access_token, response.expires_in); + }, + onError: () => { + clearToken(); + }, + }); + + const connect = useCallback(() => { + login(); + }, [login]); + + const disconnect = useCallback(() => { + googleLogout(); + clearToken(); + queryClient.removeQueries({ queryKey: [GCAL_QUERY_KEY] }); + }, [clearToken, queryClient]); + + const fetchEventsForDateRange = useCallback(async ( + startDate: string, + endDate: string, + options?: { fullDayOnly?: boolean }, + ): Promise => { + if (!isConnected || !storedToken) return []; + + const rangeStart = new Date(startDate); + rangeStart.setHours(0, 0, 0, 0); + + const rangeEnd = new Date(endDate); + rangeEnd.setHours(23, 59, 59, 999); + + const params = new URLSearchParams({ + timeMin: rangeStart.toISOString(), + timeMax: rangeEnd.toISOString(), + singleEvents: 'true', + orderBy: 'startTime', + }); + + const response = await fetch( + `${CALENDAR_API_BASE}/calendars/primary/events?${params}`, + { headers: { Authorization: `Bearer ${storedToken.accessToken}` } }, + ); + + if (!response.ok) { + if (response.status === 401) clearToken(); + return []; + } + + const data = await response.json() as { items?: GoogleCalendarEvent[] }; + const items = data.items ?? []; + if (options?.fullDayOnly) { + return items.filter((event) => !!event.start.date && !event.start.dateTime); + } + return items; + }, [isConnected, storedToken, clearToken]); + + const fetchEvents = useCallback( + (date: string, options?: { fullDayOnly?: boolean }) => ( + fetchEventsForDateRange(date, date, options) + ), + [fetchEventsForDateRange], + ); + + const value = useMemo(() => ({ + isAvailable: !!clientId, + isConnected, + expiresAt: storedToken?.expiresAt, + connect, + disconnect, + fetchEvents, + fetchEventsForDateRange, + }), [ + clientId, + isConnected, + storedToken?.expiresAt, + connect, + disconnect, + fetchEvents, + fetchEventsForDateRange, + ]); + + return ( + + {children} + + ); +} + +export default GoogleCalendarProvider; diff --git a/src/contexts/googleCalendar.tsx b/src/contexts/googleCalendar.tsx new file mode 100644 index 0000000..ea287b6 --- /dev/null +++ b/src/contexts/googleCalendar.tsx @@ -0,0 +1,55 @@ +import { createContext } from 'react'; + +export interface GoogleCalendarEvent { + id: string; + summary?: string; + description?: string; + location?: string; + status: string; + htmlLink?: string; + start: { + dateTime?: string; + date?: string; + timeZone?: string; + }; + end: { + dateTime?: string; + date?: string; + timeZone?: string; + }; +} + +export interface GoogleCalendarContextValue { + isAvailable: boolean; + isConnected: boolean; + expiresAt: number | undefined; + connect: () => void; + disconnect: () => void; + fetchEvents: ( + date: string, + options?: { fullDayOnly?: boolean }, + ) => Promise; + fetchEventsForDateRange: ( + startDate: string, + endDate: string, + options?: { fullDayOnly?: boolean }, + ) => Promise; +} + +const GoogleCalendarContext = createContext({ + isAvailable: false, + isConnected: false, + expiresAt: undefined, + connect: () => { + // eslint-disable-next-line no-console + console.warn('GoogleCalendarContext::connect called without provider'); + }, + disconnect: () => { + // eslint-disable-next-line no-console + console.warn('GoogleCalendarContext::disconnect called without provider'); + }, + fetchEvents: () => Promise.resolve([]), + fetchEventsForDateRange: () => Promise.resolve([]), +}); + +export default GoogleCalendarContext; diff --git a/src/hooks/useGoogleCalendar.ts b/src/hooks/useGoogleCalendar.ts index 1db3ef2..7f14637 100644 --- a/src/hooks/useGoogleCalendar.ts +++ b/src/hooks/useGoogleCalendar.ts @@ -1,162 +1,11 @@ -import { - useCallback, - useEffect, - useState, -} from 'react'; -import { - googleLogout, - useGoogleLogin, -} from '@react-oauth/google'; +import { useContext } from 'react'; -const STORAGE_KEY = 'timur-google-calendar-token'; -const CALENDAR_SCOPE = 'https://www.googleapis.com/auth/calendar.readonly'; -const CALENDAR_API_BASE = 'https://www.googleapis.com/calendar/v3'; +import GoogleCalendarContext, { type GoogleCalendarEvent } from '#contexts/googleCalendar'; -export interface GoogleCalendarEvent { - id: string; - summary?: string; - description?: string; - location?: string; - status: string; - htmlLink?: string; - start: { - dateTime?: string; - date?: string; - timeZone?: string; - }; - end: { - dateTime?: string; - date?: string; - timeZone?: string; - }; -} - -interface StoredToken { - accessToken: string; - expiresAt: number; -} - -function getStoredToken(): StoredToken | null { - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) return null; - return JSON.parse(raw) as StoredToken; - } catch { - return null; - } -} - -function isTokenValid(token: StoredToken | null): token is StoredToken { - if (!token) return false; - return Date.now() < token.expiresAt - 60_000; -} +export type { GoogleCalendarEvent }; function useGoogleCalendar() { - const clientId = import.meta.env.APP_GOOGLE_OAUTH_CLIENT_ID as string | undefined; - - const [storedToken, setStoredToken] = useState(() => getStoredToken()); - - const isConnected = isTokenValid(storedToken); - - // Clear token when it expires - useEffect(() => { - if (!storedToken) return undefined; - const msUntilExpiry = storedToken.expiresAt - Date.now(); - if (msUntilExpiry <= 0) { - setStoredToken(null); - return undefined; - } - const timeout = window.setTimeout(() => setStoredToken(null), msUntilExpiry); - return () => window.clearTimeout(timeout); - }, [storedToken]); - - const saveToken = useCallback((accessToken: string, expiresIn: number) => { - const token: StoredToken = { - accessToken, - expiresAt: Date.now() + expiresIn * 1000, - }; - localStorage.setItem(STORAGE_KEY, JSON.stringify(token)); - setStoredToken(token); - }, []); - - const clearToken = useCallback(() => { - localStorage.removeItem(STORAGE_KEY); - setStoredToken(null); - }, []); - - const login = useGoogleLogin({ - scope: CALENDAR_SCOPE, - onSuccess: (response) => { - saveToken(response.access_token, response.expires_in); - }, - onError: () => { - clearToken(); - }, - }); - - const connect = useCallback(() => { - login(); - }, [login]); - - const disconnect = useCallback(() => { - googleLogout(); - clearToken(); - }, [clearToken]); - - const fetchEventsForDateRange = useCallback(async ( - startDate: string, - endDate: string, - options?: { fullDayOnly?: boolean }, - ): Promise => { - if (!isConnected || !storedToken) return []; - - const rangeStart = new Date(startDate); - rangeStart.setHours(0, 0, 0, 0); - - const rangeEnd = new Date(endDate); - rangeEnd.setHours(23, 59, 59, 999); - - const params = new URLSearchParams({ - timeMin: rangeStart.toISOString(), - timeMax: rangeEnd.toISOString(), - singleEvents: 'true', - orderBy: 'startTime', - }); - - const response = await fetch( - `${CALENDAR_API_BASE}/calendars/primary/events?${params}`, - { headers: { Authorization: `Bearer ${storedToken.accessToken}` } }, - ); - - if (!response.ok) { - if (response.status === 401) clearToken(); - return []; - } - - const data = await response.json() as { items?: GoogleCalendarEvent[] }; - const items = data.items ?? []; - if (options?.fullDayOnly) { - return items.filter((event) => !!event.start.date && !event.start.dateTime); - } - return items; - }, [isConnected, storedToken, clearToken]); - - const fetchEvents = useCallback( - (date: string, options?: { fullDayOnly?: boolean }) => ( - fetchEventsForDateRange(date, date, options) - ), - [fetchEventsForDateRange], - ); - - return { - isAvailable: !!clientId, - isConnected, - expiresAt: storedToken?.expiresAt, - connect, - disconnect, - fetchEvents, - fetchEventsForDateRange, - }; + return useContext(GoogleCalendarContext); } export default useGoogleCalendar; diff --git a/src/views/DailyJournal/StartSidebar/index.tsx b/src/views/DailyJournal/StartSidebar/index.tsx index 6b50b56..7942d44 100644 --- a/src/views/DailyJournal/StartSidebar/index.tsx +++ b/src/views/DailyJournal/StartSidebar/index.tsx @@ -4,7 +4,7 @@ import { useMemo, } from 'react'; import { - RiSettingsLine, + RiGoogleFill, RiTerminalBoxLine, } from 'react-icons/ri'; import { useSuspenseQuery } from '@tanstack/react-query'; @@ -15,7 +15,6 @@ import { import Button from '#components/Button'; import DefaultMessage from '#components/DefaultMessage'; -import Link from '#components/Link'; import MonthlyCalendar from '#components/MonthlyCalendar'; import { type DayEventsAndDeadlinesQuery, @@ -145,6 +144,28 @@ function StartSidebar(props: Props) { lastEditedAt, } = props; + const { + isAvailable: isGoogleCalendarAvailable, + isConnected: isGoogleCalendarConnected, + expiresAt: googleCalendarExpiresAt, + connect: connectGoogleCalendar, + disconnect: disconnectGoogleCalendar, + } = useGoogleCalendar(); + + const googleCalendarStatusMessage = useMemo(() => { + if (!isGoogleCalendarConnected) { + return 'Connect to view events from Google Calendar. ✨'; + } + if (!googleCalendarExpiresAt) { + return 'Connected to Google Calendar.'; + } + const expiresOn = new Date(googleCalendarExpiresAt).toLocaleString([], { + dateStyle: 'medium', + timeStyle: 'short', + }); + return `Connected. Integration expires on ${expiresOn}.`; + }, [isGoogleCalendarConnected, googleCalendarExpiresAt]); + const addedDescriptions = useMemo(() => { const set = new Set(); dayWorkItems.forEach((item) => { @@ -190,14 +211,19 @@ function StartSidebar(props: Props) { > Shortcuts - } - > - Settings - + {isGoogleCalendarAvailable && ( + + )} ); diff --git a/src/views/DailyJournal/index.tsx b/src/views/DailyJournal/index.tsx index 28658d3..9249584 100644 --- a/src/views/DailyJournal/index.tsx +++ b/src/views/DailyJournal/index.tsx @@ -774,7 +774,6 @@ export function Component() { { - if (!isGoogleCalendarConnected) { - return 'Connect to view events from Google Calendar. ✨'; - } - if (!googleCalendarExpiresAt) { - return 'Connected to Google Calendar.'; - } - const expiresOn = new Date(googleCalendarExpiresAt).toLocaleString([], { - dateStyle: 'medium', - timeStyle: 'short', - }); - return `Connected. Integration expires on ${expiresOn}.`; - }, [isGoogleCalendarConnected, googleCalendarExpiresAt]); - const updateJournalGrouping = useCallback((value: number, name: 'groupLevel' | 'joinLevel') => { const oldValue = storedConfig.dailyJournalGrouping ?? defaultConfigValue.dailyJournalGrouping; @@ -774,39 +750,6 @@ export function Component() { value={storedConfig.editingMode} /> -
-

- Google Calendar -

- {!isGoogleCalendarAvailable && ( -

- Google Calendar integration requires - {' '} - APP_GOOGLE_OAUTH_CLIENT_ID - {' '} - to be configured. -

- )} - {isGoogleCalendarAvailable && ( - <> -

- {googleCalendarStatusMessage} -

- - - )} -
From 794c620cedb9212ae4f98dea8f20d40f6faf5d3a Mon Sep 17 00:00:00 2001 From: tnagorra Date: Sat, 30 May 2026 08:54:27 +0545 Subject: [PATCH 02/11] feat(standup): add links to project, contract, and deadline --- src/components/AdminEditLink/index.tsx | 45 +++++++++++++++++++ .../AdminEditLink/styles.module.css | 9 ++++ src/components/UpcomingEventsList/index.tsx | 16 +++++-- src/utils/types.ts | 1 + .../DailyStandup/ProjectSection/index.tsx | 11 ++++- 5 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 src/components/AdminEditLink/index.tsx create mode 100644 src/components/AdminEditLink/styles.module.css diff --git a/src/components/AdminEditLink/index.tsx b/src/components/AdminEditLink/index.tsx new file mode 100644 index 0000000..1a8e1b4 --- /dev/null +++ b/src/components/AdminEditLink/index.tsx @@ -0,0 +1,45 @@ +import { useContext } from 'react'; + +import UserContext from '#contexts/user'; + +import styles from './styles.module.css'; + +export type AdminEntity = 'project' | 'deadline' | 'event' | 'contract'; + +const entityAdminPath: Record = { + project: 'project/project', + deadline: 'project/deadline', + event: 'common/event', + contract: 'track/contract', +}; + +interface Props { + entity: AdminEntity; + id: string; + children: React.ReactNode; +} + +function AdminEditLink(props: Props) { + const { entity, id, children } = props; + const { userAuth } = useContext(UserContext); + + if (!userAuth?.isStaff) { + return children; + } + + const href = `${import.meta.env.APP_GRAPHQL_DOMAIN}/admin/${entityAdminPath[entity]}/${id}/change/`; + + return ( + + {children} + + ); +} + +export default AdminEditLink; diff --git a/src/components/AdminEditLink/styles.module.css b/src/components/AdminEditLink/styles.module.css new file mode 100644 index 0000000..87a3fe8 --- /dev/null +++ b/src/components/AdminEditLink/styles.module.css @@ -0,0 +1,9 @@ +.admin-edit-link { + cursor: pointer; + text-decoration: none; + color: inherit; + + &:hover { + text-decoration: underline; + } +} diff --git a/src/components/UpcomingEventsList/index.tsx b/src/components/UpcomingEventsList/index.tsx index 0b879ca..effa04c 100644 --- a/src/components/UpcomingEventsList/index.tsx +++ b/src/components/UpcomingEventsList/index.tsx @@ -15,6 +15,7 @@ import { compareNumber } from '@togglecorp/fujs'; import { type EventTypeEnum } from '#generated/types/graphql'; import { type GeneralEventType } from '#utils/types'; +import AdminEditLink from '../AdminEditLink'; import GeneralEventOutput from '../GeneralEvent'; import styles from './styles.module.css'; @@ -57,6 +58,7 @@ function UpcomingEventsList(props: Props) { () => [ ...(deadlines?.map((deadline) => ({ key: `DEADLINE-${deadline.id}`, + id: deadline.id, type: 'DEADLINE' as const, typeDisplay: 'Deadline', icon: deadline.isExternal ? : , @@ -65,6 +67,7 @@ function UpcomingEventsList(props: Props) { })) ?? []), ...(events?.map((event) => ({ key: `${event.type}-${event.id}`, + id: event.id, type: event.type, typeDisplay: event.typeDisplay, icon: eventIcons[event.type], @@ -83,10 +86,15 @@ function UpcomingEventsList(props: Props) { return ( - + + + {item.remainingDays < 0 && nextItem && nextItem.remainingDays >= 0 diff --git a/src/utils/types.ts b/src/utils/types.ts index ce0ae37..58b7237 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -70,6 +70,7 @@ export type ConfigStorage = { export interface GeneralEventType { key: string; + id: string; type: EventTypeEnum | 'DEADLINE'; typeDisplay: string; icon: React.ReactNode; diff --git a/src/views/DailyStandup/ProjectSection/index.tsx b/src/views/DailyStandup/ProjectSection/index.tsx index 18d2304..3da440e 100644 --- a/src/views/DailyStandup/ProjectSection/index.tsx +++ b/src/views/DailyStandup/ProjectSection/index.tsx @@ -9,6 +9,7 @@ import { useQuery, } from 'urql'; +import AdminEditLink from '#components/AdminEditLink'; import DefaultMessage from '#components/DefaultMessage'; import SlideCounter from '#components/SlideCounter'; import UpcomingEventsList from '#components/UpcomingEventsList'; @@ -159,7 +160,11 @@ function ProjectSection(props: Props) { + {project.name} + + )} primaryDescription={project?.description && (

{project?.description} @@ -181,7 +186,9 @@ function ProjectSection(props: Props) {

    {activeContracts?.map((contract) => (
  • - {contract.name} + + {contract.name} +
  • ))}
From fcb21749c79d853522f9846bae0a093715346471 Mon Sep 17 00:00:00 2001 From: tnagorra Date: Sat, 30 May 2026 08:56:08 +0545 Subject: [PATCH 03/11] feat: better matching for included google calendar events --- .../StartSidebar/GoogleCalendarSection/index.tsx | 8 ++++---- src/views/DailyJournal/StartSidebar/index.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/views/DailyJournal/StartSidebar/GoogleCalendarSection/index.tsx b/src/views/DailyJournal/StartSidebar/GoogleCalendarSection/index.tsx index 0a4602e..ac25bc0 100644 --- a/src/views/DailyJournal/StartSidebar/GoogleCalendarSection/index.tsx +++ b/src/views/DailyJournal/StartSidebar/GoogleCalendarSection/index.tsx @@ -123,7 +123,7 @@ function ScheduleRow(props: ScheduleRowProps) { interface Props { date: string; - addedDescriptions: Set; + addedDescriptions: string[]; onWorkItemCreateFromCalendar: (override: Partial) => void; loading?: boolean; googleEvents: GoogleCalendarEvent[]; @@ -226,9 +226,9 @@ function GoogleCalendarSection(props: Props) { )} {timedGoogleEvents.map((event, index) => { - const isAdded = event.summary - ? addedDescriptions.has(event.summary.trim().toLowerCase()) - : false; + const normalizedSummary = event.summary?.trim().toLowerCase() ?? ''; + const isAdded = normalizedSummary.length > 0 + && addedDescriptions.some((d) => d.includes(normalizedSummary)); return ( diff --git a/src/views/DailyJournal/StartSidebar/index.tsx b/src/views/DailyJournal/StartSidebar/index.tsx index 7942d44..aadc832 100644 --- a/src/views/DailyJournal/StartSidebar/index.tsx +++ b/src/views/DailyJournal/StartSidebar/index.tsx @@ -72,7 +72,7 @@ const QUERY_CONTEXT = { suspense: true } as const; interface DayEventsAndCalendarProps { selectedDate: string; - addedDescriptions: Set; + addedDescriptions: string[]; onWorkItemCreateFromCalendar: (override: Partial) => void; } @@ -167,13 +167,13 @@ function StartSidebar(props: Props) { }, [isGoogleCalendarConnected, googleCalendarExpiresAt]); const addedDescriptions = useMemo(() => { - const set = new Set(); + const list: string[] = []; dayWorkItems.forEach((item) => { if (item.description) { - set.add(item.description.trim().toLowerCase()); + list.push(item.description.trim().toLowerCase()); } }); - return set; + return list; }, [dayWorkItems]); return ( From dfe4a6bf9e3d776d5122a61eb6fa2f778542ed4a Mon Sep 17 00:00:00 2001 From: tnagorra Date: Sat, 30 May 2026 08:55:41 +0545 Subject: [PATCH 04/11] feat(standup): make right section more prominent --- src/views/DailyStandup/Slide/styles.module.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/views/DailyStandup/Slide/styles.module.css b/src/views/DailyStandup/Slide/styles.module.css index b030b98..7332bc9 100644 --- a/src/views/DailyStandup/Slide/styles.module.css +++ b/src/views/DailyStandup/Slide/styles.module.css @@ -78,11 +78,15 @@ display: flex; flex-direction: column; flex-grow: 1; + border: var(--width-separator-sm) solid var(--color-separator); + border-radius: var(--border-radius-lg); background-color: var(--color-foreground); padding: var(--spacing-lg); gap: var(--spacing-md); @media screen and (max-width: 900px) { + border: unset; + border-radius: unset; background-color: unset; padding: unset; } From 62e3d91caaed975ffd957a27938ab61323366ed8 Mon Sep 17 00:00:00 2001 From: tnagorra Date: Sat, 30 May 2026 08:56:33 +0545 Subject: [PATCH 05/11] fix: force portrait orientation for pwa --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 67c6036..cf2a7bb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -86,7 +86,7 @@ export default defineConfig(({ mode }) => { scope: '/', display: 'standalone', display_override: ['standalone'], - orientation: 'any', + orientation: 'portrait', // NOTE: keep in sync with terracotta --color-background in src/themes.css theme_color: '#fafaf0', background_color: '#fafaf0', From e6a3a2baac15d13daf2b93cf8e83bfa05a667836 Mon Sep 17 00:00:00 2001 From: tnagorra Date: Sat, 30 May 2026 08:55:13 +0545 Subject: [PATCH 06/11] feat: improve shortcuts dialog presentation --- .../DailyJournal/ShortcutsDialog/index.tsx | 88 ++++++++++--------- .../ShortcutsDialog/styles.module.css | 32 ++++++- 2 files changed, 74 insertions(+), 46 deletions(-) diff --git a/src/views/DailyJournal/ShortcutsDialog/index.tsx b/src/views/DailyJournal/ShortcutsDialog/index.tsx index 298e7a3..27ce9cd 100644 --- a/src/views/DailyJournal/ShortcutsDialog/index.tsx +++ b/src/views/DailyJournal/ShortcutsDialog/index.tsx @@ -39,55 +39,59 @@ function ShortcutsDialog(props: Props) { className={styles.shortcutsDialog} closeOnOutsideClick > -
- Hit - {' '} - Ctrl+Space - {' '} - to add a new entry. +
+ Entry +
+
+ Add a new entry
-
- Hit - {' '} - Ctrl+Enter - {' '} - to assist on the focused entry. + + Ctrl+Space + +
+ Assist on the focused entry
-
- Hit - {' '} - Ctrl+Shift+Enter - {' '} - to clone the focused entry. + + Ctrl+Enter + +
+ Clone the focused entry
-
- Hit - {' '} - Ctrl+Shift+Left - {' '} - to go to previous day. + + Ctrl+Shift+Enter + + +
+ Navigation +
+
+ Previous day
-
- Hit - {' '} - Ctrl+Shift+Right - {' '} - to go to next day. + + Ctrl+Shift+Left + +
+ Next day
-
- Hit - {' '} - Ctrl+Shift+Down - {' '} - to go to present day. + + Ctrl+Shift+Right + +
+ Present day
-
- Hit - {' '} - Ctrl+Shift+? - {' '} - to view shortcuts. + + Ctrl+Shift+Down + + +
+ Help +
+
+ View shortcuts
+ + Ctrl+Shift+? + ); } diff --git a/src/views/DailyJournal/ShortcutsDialog/styles.module.css b/src/views/DailyJournal/ShortcutsDialog/styles.module.css index 0977bfc..c1103f0 100644 --- a/src/views/DailyJournal/ShortcutsDialog/styles.module.css +++ b/src/views/DailyJournal/ShortcutsDialog/styles.module.css @@ -1,8 +1,32 @@ .shortcuts-dialog { .modal-content { - display: flex; - flex-direction: column; - gap: var(--spacing-md); - font-size: var(--font-size-md); + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + column-gap: var(--spacing-md); + row-gap: var(--spacing-sm); + + .subheading { + padding-top: var(--spacing-md); + grid-column: 1 / -1; + } + + .description { + grid-column: 1; + } + + .key { + display: inline-flex; + grid-column: 3; + justify-self: end; + align-items: center; + border: var(--width-separator-sm) solid var(--color-separator); + border-radius: var(--border-radius-md); + background-color: var(--color-foreground); + padding: var(--spacing-3xs) var(--spacing-xs); + color: var(--color-primary-text); + font-family: var(--font-family-mono); + font-size: var(--font-size-sm); + } } } From 0bc25e59fc427ee09bbf895bda0294449f0dec4d Mon Sep 17 00:00:00 2001 From: tnagorra Date: Sat, 30 May 2026 09:09:11 +0545 Subject: [PATCH 07/11] fix: increase size of calendar popups --- src/views/DailyJournal/DayView/WorkItemRow/styles.module.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/views/DailyJournal/DayView/WorkItemRow/styles.module.css b/src/views/DailyJournal/DayView/WorkItemRow/styles.module.css index cf47299..a47b8b0 100644 --- a/src/views/DailyJournal/DayView/WorkItemRow/styles.module.css +++ b/src/views/DailyJournal/DayView/WorkItemRow/styles.module.css @@ -93,5 +93,6 @@ flex-direction: column; gap: var(--spacing-md); padding: var(--spacing-md); + min-width: 24rem; } } From 2707ef483a8c22502b9cbc0c6ee2c4f1414435ee Mon Sep 17 00:00:00 2001 From: tnagorra Date: Sat, 30 May 2026 09:08:16 +0545 Subject: [PATCH 08/11] feat: use linear-regression model for work item classififcation --- src/utils/workItemClassifier/index.ts | 126 + src/utils/workItemClassifier/model.json | 7892 +++++++++++++++++++++++ src/utils/workItemClassifier/types.ts | 25 + src/views/DailyJournal/index.tsx | 52 +- 4 files changed, 8044 insertions(+), 51 deletions(-) create mode 100644 src/utils/workItemClassifier/index.ts create mode 100644 src/utils/workItemClassifier/model.json create mode 100644 src/utils/workItemClassifier/types.ts diff --git a/src/utils/workItemClassifier/index.ts b/src/utils/workItemClassifier/index.ts new file mode 100644 index 0000000..dbbed9d --- /dev/null +++ b/src/utils/workItemClassifier/index.ts @@ -0,0 +1,126 @@ +// Work-item type classifier — inference side. +// +// Reads a logistic-regression model trained externally (see scripts/README.md +// for the training pipeline and design decisions) and suggests a +// TimeEntryTypeEnum value for a given description. Returns `undefined` when +// the top-class probability is below that class's threshold. +// +// Train/serve contract — this code MUST stay in sync with the analyzer in +// scripts/train_work_item_classifier.py. Specifically: +// - lowercase +// - tokenize on /[^a-z0-9]+/ +// - emit unigrams + adjacent bigrams (bigrams joined with '_') +// - apply IDF + L2 normalize (TF-IDF features) +// The training side additionally substitutes entity names (people, projects, +// clients) with sentinel tokens; this frontend does NOT — entity tokens in +// real descriptions just miss the vocab and are silently ignored, which is +// the desired privacy-preserving behavior. +import type { TimeEntryTypeEnum } from '#generated/types/graphql'; + +import type { WorkItemClassifierModel } from './types'; + +import modelData from './model.json'; + +const model = modelData as unknown as WorkItemClassifierModel; + +const vocabIndex = new Map( + model.vocabulary.map((token, idx) => [token, idx]), +); + +function tokenize(text: string): string[] { + return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0); +} + +function buildFeatures(text: string): Map { + // Mirrors sklearn TfidfTransformer(norm='l2', use_idf=True, smooth_idf=True, + // sublinear_tf=False) applied to count features. Steps: + // 1. Tokenize (unigrams + adjacent bigrams) + // 2. Multiply each count by the token's IDF (from model.idf) + // 3. L2-normalize the row vector + const tokens = tokenize(text); + const rawCounts = new Map(); + + const bump = (token: string) => { + const idx = vocabIndex.get(token); + if (idx !== undefined) { + rawCounts.set(idx, (rawCounts.get(idx) ?? 0) + 1); + } + }; + + tokens.forEach((tok) => bump(tok)); + for (let i = 0; i + 1 < tokens.length; i += 1) { + const a = tokens[i]; + const b = tokens[i + 1]; + if (a !== undefined && b !== undefined) { + bump(`${a}_${b}`); + } + } + + // tf × idf + const tfidf = new Map(); + rawCounts.forEach((count, idx) => { + const idf = model.idf[idx] ?? 0; + tfidf.set(idx, count * idf); + }); + + // L2 normalize + let normSquared = 0; + tfidf.forEach((v) => { normSquared += v * v; }); + const norm = Math.sqrt(normSquared); + if (norm > 0) { + tfidf.forEach((v, idx) => { tfidf.set(idx, v / norm); }); + } + + return tfidf; +} + +function inferTypeFromDescription( + description: string, +): TimeEntryTypeEnum | undefined { + if (description.trim().length === 0) { + return undefined; + } + + const counts = buildFeatures(description); + if (counts.size === 0) { + return undefined; + } + + const scores = model.intercepts.map((intercept, classIdx) => { + let s = intercept; + const classWeights = model.weights[classIdx]; + if (classWeights) { + classWeights.forEach(([tokenIdx, weight]) => { + const cnt = counts.get(tokenIdx); + if (cnt !== undefined) { + s += weight * cnt; + } + }); + } + return s; + }); + + const maxScore = Math.max(...scores); + const exps = scores.map((s) => Math.exp(s - maxScore)); + const sum = exps.reduce((a, b) => a + b, 0); + + let bestIdx = 0; + let bestExp = exps[0] ?? 0; + for (let c = 1; c < exps.length; c += 1) { + const e = exps[c]; + if (e !== undefined && e > bestExp) { + bestExp = e; + bestIdx = c; + } + } + + const bestProb = bestExp / sum; + const classThreshold = model.thresholds[bestIdx] ?? 1.0; + if (bestProb < classThreshold) { + return undefined; + } + + return model.classes[bestIdx]; +} + +export default inferTypeFromDescription; diff --git a/src/utils/workItemClassifier/model.json b/src/utils/workItemClassifier/model.json new file mode 100644 index 0000000..4d5f2f0 --- /dev/null +++ b/src/utils/workItemClassifier/model.json @@ -0,0 +1,7892 @@ +{ + "classes": [ + "ANNOTATION", + "DESIGN", + "DEVELOPMENT", + "DEV_OPS", + "DOCUMENTATION", + "EXTERNAL_DISCUSSION", + "EXTERNAL_MEETING", + "INTERNAL_DISCUSSION", + "INTERNAL_MEETING", + "OPERATION", + "PROJECT_MANAGEMENT", + "RESEARCH", + "REVIEW", + "TESTING" + ], + "vocabulary": [ + "0", + "0_to", + "1", + "1_1", + "2", + "24", + "7", + "a", + "a_document", + "about", + "about_client", + "about_the", + "access", + "according", + "according_to", + "account", + "action", + "action_items", + "add", + "add_changes", + "add_edit", + "add_test", + "added", + "adding", + "admin", + "admin_panel", + "after", + "after_deployment", + "after_effects", + "ai", + "ai_chatbot", + "alert", + "all", + "all_hands", + "all_the", + "alpha", + "alpha_2", + "alpha_and", + "alpha_deploy", + "alpha_deployment", + "alpha_instance", + "analysis", + "analyst", + "and", + "and_add", + "and_assign", + "and_changes", + "and_client", + "and_colleague", + "and_components", + "and_create", + "and_deploy", + "and_deployed", + "and_deployment", + "and_design", + "and_discussion", + "and_email", + "and_fix", + "and_forth", + "and_get", + "and_kick", + "and_merge", + "and_planning", + "and_pr", + "and_qa", + "and_reply", + "and_research", + "and_share", + "and_test", + "and_tested", + "and_the", + "and_update", + "anniversary", + "anniversary_celebration", + "api", + "api_and", + "api_pull", + "app", + "apps", + "argo", + "argocd", + "as", + "as_per", + "ask", + "assets", + "assign", + "assist", + "assisted", + "attend", + "attend_project", + "autolayout", + "automation", + "aws", + "azure", + "back", + "back_and", + "back_to", + "backend", + "backend_pull", + "base", + "bcp", + "be", + "behavior", + "behavior_document", + "bitnami", + "board", + "board_and", + "board_updates", + "boards", + "book", + "booklet", + "booklet_design", + "branch", + "branch_and", + "brand", + "brand_book", + "branding", + "bug", + "bug_fixes", + "bugs", + "bulk", + "bulk_upload", + "by", + "by_client", + "by_colleague", + "cacheppuccino", + "call", + "call_with", + "canva", + "canvas", + "card", + "case", + "case_according", + "case_for", + "cases", + "cases_for", + "catch", + "catch_up", + "catchup", + "catchup_and", + "catchup_call", + "catchup_meeting", + "catchup_on", + "catchup_with", + "catchups", + "celebration", + "celery", + "change", + "changelog", + "changelogs", + "changes", + "changes_according", + "changes_as", + "changes_made", + "changes_on", + "changes_to", + "channel", + "chart", + "chart_for", + "charts", + "chat", + "chat_bot", + "chatbot", + "chatbot_backend", + "check", + "checklist", + "checklist_for", + "ci", + "cleanup", + "client", + "client_about", + "client_and", + "client_call", + "client_catchup", + "client_client", + "client_for", + "client_meeting", + "client_over", + "client_regarding", + "client_s", + "client_team", + "client_to", + "client_website", + "client_weekly", + "clients", + "clients_about", + "close", + "cluster", + "cnpg", + "code", + "code_review", + "codebase", + "colleague", + "colleague_about", + "colleague_and", + "colleague_anniversary", + "colleague_colleague", + "colleague_dai", + "colleague_for", + "colleague_in", + "colleague_on", + "colleague_pr", + "colleague_regarding", + "colleague_s", + "colleague_with", + "color", + "color_palette", + "colors", + "com", + "com_project", + "com_the", + "com_toggle", + "command", + "comment", + "comments", + "commit", + "comms", + "comms_with", + "community", + "community_call", + "complete", + "completed", + "component", + "components", + "components_and", + "config", + "configs", + "configuration", + "content", + "continue", + "continue_working", + "continued", + "continued_working", + "continues", + "contract", + "corp", + "corp_ai", + "course", + "cover", + "cover_page", + "create", + "create_a", + "create_github", + "create_issues", + "create_ticket", + "create_tickets", + "created", + "creating", + "creation", + "credentials", + "csv", + "custom", + "dai", + "dai_about", + "dai_and", + "dai_colleague", + "daily", + "daily_catchup", + "daily_stand", + "daily_standup", + "dashboard", + "data", + "data_validation", + "database", + "day", + "db", + "deadline", + "deadlines", + "deadlines_in", + "deaign", + "debug", + "debugging", + "deploy", + "deploy_changes", + "deploy_dref", + "deploy_latest", + "deploy_on", + "deploy_the", + "deploy_to", + "deployed", + "deployed_the", + "deployed_to", + "deployment", + "deployment_and", + "deployment_check", + "deployment_checklist", + "deployment_issue", + "deployment_issues", + "deployment_notice", + "deployment_of", + "deployment_on", + "deployment_plan", + "deployment_to", + "deployment_with", + "deployments", + "design", + "design_and", + "design_changes", + "design_for", + "design_process", + "design_research", + "design_started", + "design_tokens", + "designs", + "desinventar", + "dev", + "dev_mode", + "dev_sprint", + "develop", + "developer", + "development", + "di", + "diagram", + "different", + "discuss", + "discuss_about", + "discuss_with", + "discussed", + "discussion", + "discussion_about", + "discussion_and", + "discussion_meeting", + "discussion_on", + "discussion_regarding", + "discussion_with", + "discussions", + "discussions_with", + "django", + "doc", + "docker", + "docs", + "document", + "document_for", + "document_with", + "documentation", + "documentation_for", + "documentation_of", + "documentation_on", + "documentation_with", + "documented", + "documented_the", + "documents", + "draft", + "dref", + "due", + "due_to", + "dummy", + "dummy_data", + "dump", + "eap", + "edit", + "edit_profile", + "editor", + "edits", + "effects", + "email", + "email_notification", + "email_to", + "emails", + "emergency", + "environment", + "eoapi", + "error", + "eru", + "eru_readiness", + "estimates", + "estimation", + "estimations", + "etl", + "event", + "exploration", + "explore", + "explore_the", + "export", + "external", + "extraction", + "farewell", + "feature", + "feedback", + "feedback_sheet", + "feedbacks", + "feedbacks_and", + "fetch", + "fid", + "fid_design", + "field", + "field_report", + "fields", + "figma", + "file", + "files", + "filter", + "filters", + "finalize", + "fine", + "firebase", + "firebase_deployment", + "fix", + "fix_issue", + "fix_issues", + "fix_pr", + "fix_sentry", + "fix_the", + "fixed", + "fixed_issues", + "fixes", + "fixes_and", + "flexbox", + "flow", + "follow", + "follow_up", + "follow_ups", + "font", + "for", + "for_client", + "for_colleague", + "for_community", + "for_k8s", + "for_meeting", + "for_the", + "form", + "forth", + "found", + "from", + "from_client", + "from_colleague", + "functionality", + "gather", + "gdacs", + "generate", + "generation", + "geocoding", + "get", + "get_back", + "github", + "github_board", + "github_com", + "global", + "global_board", + "google", + "google_doc", + "google_sheet", + "gpu", + "graphql", + "hands", + "helm", + "helm_chart", + "help", + "help_colleague", + "help_of", + "high", + "high_fid", + "holiday", + "home", + "home_page", + "homepage", + "hook", + "hotfix", + "hours", + "how", + "how_to", + "https", + "https_github", + "humanitarian", + "icons", + "id", + "idu", + "ii", + "illustration", + "illustrations", + "implement", + "implementation", + "implementation_document", + "implementation_of", + "improve", + "in", + "in_alpha", + "in_channel", + "in_github", + "in_global", + "in_k8s", + "in_project", + "in_slack", + "in_staging", + "in_the", + "in_wikijs", + "include", + "infographics", + "ingress", + "inspiration", + "inspirations", + "instance", + "instance_and", + "instances", + "integration", + "internal", + "internal_catchup", + "internal_discussion", + "internally", + "internship", + "interview", + "into", + "into_client", + "into_the", + "investigate", + "issue", + "issue_discussion", + "issue_fix", + "issue_in", + "issue_on", + "issue_with", + "issues", + "issues_for", + "istqb", + "it", + "items", + "jba", + "ji", + "k8s", + "k8s_apps", + "key", + "keynotes", + "kick", + "kick_off", + "kickoff", + "kubernetes", + "latest", + "latest_changes", + "learning", + "let", + "libraries", + "library", + "link", + "links", + "list", + "listing", + "lists", + "load", + "local", + "local_unit", + "locally", + "logging", + "logic", + "login", + "logo", + "lolo", + "look", + "look_into", + "low", + "low_fid", + "made", + "maintenance", + "making", + "manual", + "markdown", + "markdown_editor", + "meeting", + "meeting_and", + "meeting_for", + "meeting_notes", + "meeting_on", + "meeting_regarding", + "meeting_with", + "merch", + "merge", + "merge_and", + "merge_https", + "message", + "message_in", + "methodology", + "migrate", + "migration", + "mini", + "minor", + "minor_discussion", + "minor_ui", + "mobile", + "mockup", + "modal", + "mode", + "model", + "model_and", + "models", + "montandon", + "montandon_etl", + "monthly", + "monthly_report", + "move", + "mutation", + "mutation_for", + "mutations", + "name", + "namespace", + "new", + "next", + "next_steps", + "nginx", + "nlrc", + "not", + "notes", + "notes_from", + "notice", + "notification", + "of", + "of_all", + "of_colleague", + "of_the", + "off", + "oliver", + "oliver_for", + "omran", + "on", + "on_adding", + "on_ai", + "on_alpha", + "on_deployment", + "on_django", + "on_feedbacks", + "on_figma", + "on_helm", + "on_how", + "on_prod", + "on_staging", + "on_the", + "onboarding", + "ops", + "ops_learning", + "out", + "out_to", + "over", + "page", + "page_design", + "pages", + "pagination", + "palette", + "panel", + "password", + "pc", + "pdc", + "pdf", + "pdf_export", + "permission", + "phase", + "pipeline", + "plan", + "planning", + "planning_meeting", + "platform", + "pm", + "pm_x", + "pms", + "popup", + "post", + "post_deployment", + "post_design", + "postgres", + "pr", + "pr_and", + "pr_comment", + "pr_comments", + "pr_fixes", + "pr_for", + "pr_review", + "pre", + "pre_commit", + "pre_meeting", + "prep", + "preparation", + "prepare", + "prepare_for", + "prepared", + "presentation", + "presentation_for", + "presentation_prep", + "process", + "prod", + "prod_deployment", + "production", + "production_deployment", + "profile", + "programming", + "project", + "project_api", + "project_backend", + "project_board", + "project_branch", + "project_call", + "project_catch", + "project_catchup", + "project_deploy", + "project_deployment", + "project_discussion", + "project_in", + "project_meeting", + "project_server", + "project_through", + "project_weekly", + "projects", + "proposal", + "proxy", + "prs", + "pull", + "pull_request", + "push", + "qa", + "qa_catch", + "qa_catchup", + "qa_team", + "query", + "question", + "questions", + "quick", + "quick_catchup", + "quick_chat", + "quick_discussion", + "reach", + "reach_out", + "read", + "readiness", + "rebase", + "recovery", + "refactor", + "refactored", + "regarding", + "regarding_the", + "related", + "related_discussion", + "related_to", + "release", + "remaining_designs", + "remove", + "reply", + "reply_to", + "repo", + "report", + "request", + "request_review", + "research", + "research_about", + "research_and", + "research_continued", + "research_for", + "research_on", + "researched", + "researched_on", + "resolve", + "retro", + "review", + "review_and", + "review_colleague", + "review_documentation", + "review_dref", + "review_field", + "review_fix", + "review_fixes", + "review_follow", + "review_https", + "review_merge", + "review_of", + "review_pr", + "review_pull", + "review_the", + "review_with", + "reviewed", + "rolling", + "rolling_notes", + "rollout", + "ruff", + "run", + "s", + "s3", + "s_anniversary", + "s_comments", + "s_email", + "s_issue", + "s_pr", + "schema", + "script", + "script_to", + "sdt", + "section", + "send", + "send_email", + "send_pr", + "sent", + "sentry", + "serve", + "server", + "server_pull", + "session", + "setup", + "share", + "share_keynotes", + "share_notes", + "share_the", + "share_with", + "shared", + "sheet", + "sheets", + "short", + "signature", + "slack", + "slack_channel", + "small", + "smrats", + "source", + "sources", + "sprint", + "sprint_planning", + "sprints", + "staging", + "staging_and", + "staging_deployment", + "stand", + "stand_up", + "standup", + "standup_and", + "start", + "started", + "started_working", + "status", + "storage", + "stories", + "studied", + "study", + "styling", + "superset", + "sync", + "sync_up", + "system", + "table", + "table_and", + "talk", + "talos", + "tasks", + "tc", + "team", + "team_catchup", + "team_page", + "team_regarding", + "team_talk", + "technical", + "teleport", + "template", + "templates", + "terraform", + "test", + "test_case", + "test_cases", + "test_of", + "test_plan", + "test_the", + "testcase", + "tested", + "tested_the", + "testimonial", + "testing", + "testing_and", + "testing_of", + "testing_started", + "testing_with", + "tests", + "the", + "the_alert", + "the_alpha", + "the_deployment", + "the_documentation", + "the_email", + "the_fixed", + "the_help", + "the_latest", + "the_meeting", + "the_pr", + "the_test", + "the_usage", + "through", + "ticket", + "ticket_for", + "tickets", + "tickets_for", + "tickets_in", + "timeline", + "timesheets", + "to", + "to_alpha", + "to_client", + "to_clients", + "to_colleague", + "to_include", + "to_k8s", + "to_oliver", + "to_production", + "to_project", + "to_staging", + "to_the", + "to_wikijs", + "toggle", + "toggle_corp", + "toi", + "tokens", + "tomorrow", + "tool", + "tools", + "transformation", + "translation", + "tried", + "troubleshoot", + "tutorials", + "type", + "typography", + "uat", + "ui", + "ui_cleanup", + "ui_design", + "unit", + "up", + "up_and", + "up_meeting", + "up_remaining", + "up_with", + "update", + "update_and", + "update_documentation", + "update_github", + "update_project", + "update_test", + "update_the", + "updated", + "updated_the", + "updates", + "updates_to", + "upgrade", + "upgrade_sentry", + "upgrade_teleport", + "upgrades", + "upload", + "ups", + "uptime", + "url", + "usage", + "usage_of", + "usages", + "usaid", + "use", + "user", + "user_stories", + "users", + "usgs", + "using", + "uv", + "v7", + "validate", + "validation", + "variables", + "variants", + "vaultwarden", + "verification", + "verified", + "verify", + "via", + "vm", + "vs", + "web_app", + "website", + "website_components", + "weekly", + "weekly_catch", + "weekly_catchup", + "weekly_client", + "weekly_project", + "went", + "went_through", + "wikijs", + "wireframe", + "wireframes", + "with", + "with_client", + "with_clients", + "with_colleague", + "with_jba", + "with_team", + "with_the", + "work", + "work_on", + "worked", + "worked_on", + "workflow", + "working", + "working_on", + "works", + "workshop", + "wrap", + "wrap_up", + "wrapped", + "wrapped_up", + "wrapup", + "write", + "write_test", + "x", + "yesterday" + ], + "idf": [ + 6.4707, + 8.6492, + 5.9983, + 7.0398, + 6.0972, + 8.6492, + 7.8019, + 5.4642, + 8.3615, + 4.1738, + 7.876, + 5.5357, + 6.1643, + 7.2221, + 7.3499, + 7.0737, + 5.8358, + 6.1643, + 4.1988, + 7.3965, + 7.9561, + 8.0431, + 7.3055, + 6.9146, + 6.8301, + 7.5506, + 6.2069, + 8.6492, + 8.2438, + 6.4897, + 7.3965, + 6.5492, + 5.6535, + 6.5908, + 7.0737, + 4.7439, + 7.6684, + 7.6684, + 8.4951, + 6.7274, + 6.0715, + 6.4707, + 7.7329, + 2.4359, + 6.2215, + 7.9561, + 7.4452, + 6.4156, + 4.7339, + 8.6492, + 7.2221, + 7.1829, + 7.8019, + 7.2629, + 7.6684, + 6.3805, + 7.4452, + 6.8034, + 7.2221, + 7.876, + 7.876, + 6.3138, + 7.0398, + 7.8019, + 7.0398, + 7.4452, + 7.1829, + 5.9636, + 6.6568, + 8.0431, + 7.0737, + 6.0465, + 5.5889, + 6.5492, + 5.1578, + 7.3499, + 7.2629, + 5.8062, + 7.6684, + 7.876, + 6.7521, + 6.4897, + 7.876, + 7.3055, + 7.6684, + 7.3499, + 6.9752, + 8.0431, + 5.3596, + 7.2221, + 8.6492, + 7.4452, + 6.0221, + 6.8856, + 6.1783, + 7.0398, + 7.3499, + 5.5137, + 7.4965, + 7.1829, + 8.4951, + 6.4707, + 6.8575, + 7.6078, + 7.2629, + 5.1427, + 7.3499, + 7.5506, + 7.6078, + 7.2629, + 8.3615, + 8.4951, + 6.8034, + 8.0431, + 7.8019, + 7.9561, + 7.5506, + 6.4156, + 7.8019, + 6.5698, + 6.3634, + 7.1088, + 5.2648, + 7.2629, + 5.9636, + 7.7329, + 4.9036, + 6.2821, + 8.1384, + 7.6078, + 7.9561, + 6.3979, + 8.8315, + 7.8019, + 6.2821, + 7.6684, + 4.9899, + 5.0249, + 4.0457, + 6.5908, + 6.9445, + 6.3979, + 7.4965, + 5.404, + 7.3055, + 6.2666, + 7.3055, + 6.2215, + 7.1829, + 8.0431, + 4.5438, + 8.6492, + 8.8315, + 7.9561, + 6.3301, + 6.8575, + 5.9412, + 6.1783, + 8.2438, + 7.0737, + 7.0737, + 7.8019, + 6.2978, + 8.2438, + 4.8026, + 6.4156, + 8.3615, + 6.529, + 6.4897, + 3.5382, + 6.5698, + 6.2513, + 7.3965, + 8.2438, + 7.3055, + 7.5506, + 7.3499, + 7.6684, + 7.3055, + 5.8663, + 7.7329, + 7.7329, + 7.8019, + 8.2438, + 6.1369, + 8.2438, + 7.6078, + 6.8856, + 7.876, + 6.0102, + 8.1384, + 7.3055, + 2.6331, + 5.9084, + 4.7044, + 7.6078, + 5.4505, + 4.4175, + 6.2363, + 8.3615, + 6.2215, + 7.876, + 5.3658, + 5.6207, + 5.9412, + 7.3499, + 8.1384, + 8.2438, + 5.8663, + 6.8301, + 8.8315, + 7.6078, + 7.876, + 7.3499, + 6.0465, + 6.9146, + 6.8856, + 7.7329, + 6.5092, + 7.6078, + 7.4452, + 6.7274, + 7.1829, + 6.2978, + 8.1384, + 6.5908, + 8.1384, + 6.8575, + 6.2666, + 6.2363, + 7.9561, + 6.3138, + 7.7329, + 8.6492, + 6.6568, + 7.6078, + 8.3615, + 7.876, + 8.2438, + 8.6492, + 4.7207, + 6.5092, + 7.6684, + 7.3499, + 7.6684, + 7.1088, + 6.7274, + 7.4452, + 6.1925, + 6.8575, + 7.2629, + 6.8856, + 4.3273, + 5.7588, + 6.2363, + 7.4452, + 6.2363, + 7.6078, + 7.3499, + 7.6078, + 5.787, + 4.8315, + 8.8315, + 6.529, + 6.7774, + 6.5492, + 7.2629, + 6.9752, + 8.1384, + 7.4965, + 6.3979, + 7.1829, + 5.3534, + 8.6492, + 8.6492, + 7.5506, + 8.4951, + 8.2438, + 7.1088, + 6.8575, + 8.1384, + 8.2438, + 4.4055, + 7.0737, + 8.6492, + 7.9561, + 8.6492, + 8.3615, + 8.4951, + 7.6684, + 7.4452, + 8.1384, + 7.6078, + 7.6078, + 7.2629, + 4.6358, + 7.3055, + 7.876, + 7.6078, + 8.2438, + 7.9561, + 8.8315, + 8.0431, + 6.5698, + 8.1384, + 5.5431, + 8.1384, + 7.1451, + 7.2629, + 7.6684, + 5.9192, + 7.7329, + 7.4965, + 6.9445, + 4.2007, + 7.007, + 5.237, + 7.3499, + 3.3526, + 6.3805, + 6.7274, + 7.7329, + 6.8575, + 6.9445, + 4.0971, + 5.7136, + 7.0737, + 5.9751, + 6.5698, + 6.452, + 6.7274, + 4.9115, + 7.2629, + 8.2438, + 5.0939, + 7.2221, + 7.8019, + 7.9561, + 8.2438, + 8.2438, + 8.8315, + 6.5092, + 6.7521, + 5.1783, + 7.7329, + 7.7329, + 7.7329, + 8.1384, + 6.9146, + 5.6788, + 6.3301, + 7.9561, + 7.8019, + 8.1384, + 8.2438, + 4.615, + 6.9752, + 7.1829, + 6.9146, + 6.3979, + 7.2629, + 7.007, + 6.5698, + 6.0465, + 6.7774, + 6.5092, + 6.3138, + 6.8301, + 6.9445, + 6.1369, + 7.2221, + 7.0398, + 8.8315, + 5.696, + 6.5698, + 6.4897, + 7.3055, + 5.9636, + 5.787, + 7.5506, + 5.9523, + 8.4951, + 7.876, + 7.4965, + 8.4951, + 5.9192, + 6.7274, + 6.9752, + 6.5698, + 6.0589, + 6.6123, + 6.5698, + 7.0737, + 6.2363, + 8.1384, + 6.2513, + 8.6492, + 4.3983, + 7.0398, + 7.8019, + 8.0431, + 8.0431, + 7.3499, + 6.8856, + 8.3615, + 5.2592, + 7.3055, + 8.6492, + 6.4336, + 6.0972, + 6.2821, + 7.876, + 8.2438, + 3.108, + 6.3805, + 7.4452, + 7.9561, + 8.6492, + 7.876, + 5.3721, + 6.6568, + 7.1451, + 8.2438, + 5.0383, + 7.3965, + 7.6078, + 7.1088, + 7.876, + 7.4452, + 7.2629, + 6.7274, + 8.1384, + 6.7774, + 7.3055, + 4.9438, + 6.5698, + 6.2513, + 6.0972, + 6.4336, + 6.4897, + 7.9561, + 7.6684, + 7.876, + 6.9445, + 6.5698, + 5.7681, + 6.7774, + 5.1327, + 5.7588, + 7.4965, + 7.1829, + 7.876, + 7.4965, + 7.9561, + 8.1384, + 7.2629, + 7.8019, + 7.3055, + 6.3634, + 6.2821, + 6.529, + 5.816, + 6.2513, + 8.3615, + 7.3965, + 7.1088, + 7.6078, + 7.2629, + 7.3055, + 8.0431, + 6.8301, + 5.9983, + 8.6492, + 7.2221, + 7.7329, + 3.8757, + 6.8301, + 7.2629, + 7.7329, + 7.6684, + 8.2438, + 6.0972, + 7.1088, + 7.007, + 6.1505, + 8.2438, + 7.3965, + 7.9561, + 7.4965, + 8.0431, + 8.4951, + 5.8259, + 8.0431, + 6.8856, + 5.8766, + 5.6288, + 7.7329, + 7.6684, + 7.2629, + 6.8034, + 7.4965, + 5.437, + 7.1451, + 6.6343, + 7.8019, + 4.0113, + 7.876, + 7.5506, + 6.5698, + 6.6343, + 5.856, + 4.7472, + 7.7329, + 7.876, + 6.3301, + 5.7405, + 7.2221, + 7.876, + 6.0715, + 8.4951, + 7.1451, + 7.3055, + 6.2821, + 6.3466, + 6.5092, + 7.1451, + 5.9751, + 6.8856, + 6.0843, + 7.6684, + 8.1384, + 7.2221, + 6.7033, + 7.3965, + 6.3466, + 7.6078, + 7.1829, + 6.7774, + 5.3472, + 6.7274, + 6.9752, + 8.0431, + 6.1505, + 7.1451, + 7.6078, + 7.6684, + 5.4105, + 5.5889, + 8.1384, + 8.4951, + 7.1088, + 7.1829, + 7.7329, + 7.8019, + 7.3965, + 7.9561, + 3.6453, + 7.4452, + 6.8856, + 7.0398, + 7.7329, + 7.007, + 4.7955, + 7.7329, + 5.696, + 8.1384, + 8.8315, + 5.9412, + 7.9561, + 8.3615, + 7.2629, + 5.856, + 7.4452, + 6.3805, + 7.4965, + 8.3615, + 7.3499, + 8.8315, + 7.8019, + 7.7329, + 5.9866, + 7.8019, + 6.9445, + 5.4642, + 8.3615, + 7.3965, + 7.7329, + 6.8575, + 6.7774, + 8.0431, + 7.5506, + 6.9146, + 8.0431, + 5.0204, + 6.4707, + 7.1088, + 7.6684, + 7.876, + 6.452, + 5.3784, + 7.1829, + 8.2438, + 6.6343, + 4.0324, + 8.3615, + 6.9752, + 6.0715, + 6.2215, + 6.8575, + 8.4951, + 7.5506, + 3.3246, + 7.8019, + 8.3615, + 6.3979, + 8.4951, + 8.1384, + 7.876, + 8.4951, + 8.3615, + 7.007, + 8.3615, + 6.7774, + 5.2648, + 6.0465, + 6.3979, + 6.529, + 6.4707, + 7.6078, + 6.8575, + 5.4505, + 7.8019, + 6.2666, + 6.9752, + 8.1384, + 7.3965, + 7.3055, + 7.5506, + 7.3965, + 6.8301, + 7.4965, + 6.2363, + 6.7033, + 6.9752, + 5.4303, + 5.0029, + 6.6798, + 7.1829, + 5.696, + 6.5698, + 6.6798, + 8.3615, + 6.1505, + 7.9561, + 8.3615, + 7.7329, + 4.2425, + 7.1088, + 7.876, + 7.007, + 7.6078, + 6.7033, + 5.3411, + 5.9751, + 7.0737, + 7.6684, + 6.1235, + 6.5908, + 5.3658, + 6.1505, + 7.2629, + 5.7048, + 8.3615, + 8.0431, + 6.3466, + 6.2215, + 7.4452, + 6.0715, + 7.4452, + 7.3055, + 7.9561, + 3.1034, + 6.6798, + 7.4452, + 7.0398, + 7.4965, + 8.0431, + 7.4452, + 6.5092, + 7.6078, + 7.6684, + 7.2221, + 7.9561, + 7.876, + 7.9561, + 6.4897, + 7.3055, + 6.2666, + 5.9983, + 6.9445, + 6.6798, + 5.6535, + 6.2978, + 7.0398, + 4.8537, + 7.3055, + 7.6078, + 7.3055, + 6.2069, + 6.8575, + 6.3138, + 5.7225, + 7.1088, + 7.8019, + 7.7329, + 7.6078, + 7.6078, + 7.1088, + 6.6123, + 6.5908, + 8.4951, + 6.1643, + 8.1384, + 4.6789, + 5.6207, + 5.5506, + 7.4452, + 6.3979, + 6.3301, + 8.8315, + 6.9146, + 5.8062, + 6.1235, + 6.8301, + 5.4922, + 5.9412, + 7.6078, + 4.6789, + 6.6568, + 6.8034, + 8.8315, + 7.2221, + 5.8977, + 6.8575, + 7.5506, + 7.8019, + 7.1829, + 3.8899, + 5.7966, + 6.5492, + 8.2438, + 8.8315, + 8.6492, + 8.0431, + 8.8315, + 8.6492, + 8.1384, + 7.9561, + 7.6684, + 6.6123, + 8.0431, + 6.6568, + 7.3965, + 7.1829, + 8.0431, + 8.1384, + 7.876, + 7.7329, + 6.5092, + 4.4697, + 7.3965, + 6.6568, + 8.1384, + 7.6078, + 7.876, + 7.3965, + 7.0737, + 5.8358, + 7.6078, + 6.529, + 7.4452, + 5.6288, + 7.6684, + 8.0431, + 7.1829, + 6.1505, + 7.2629, + 6.0465, + 8.6492, + 5.8358, + 4.6852, + 4.6419, + 7.3055, + 6.8575, + 7.6684, + 6.9752, + 6.9146, + 6.2978, + 7.9561, + 7.3055, + 8.2438, + 5.3411, + 7.876, + 6.5092, + 7.6078, + 6.4707, + 6.9752, + 5.9301, + 7.7329, + 7.3499, + 5.0204, + 7.5506, + 6.3805, + 6.6123, + 6.6123, + 6.4707, + 7.8019, + 6.6343, + 6.0465, + 7.3965, + 5.9301, + 7.1829, + 7.876, + 8.6492, + 6.7521, + 7.4965, + 7.4965, + 5.637, + 7.1451, + 6.9445, + 6.6568, + 8.1384, + 7.3055, + 7.7329, + 5.696, + 6.0972, + 4.7372, + 7.6078, + 8.8315, + 7.4965, + 8.3615, + 6.9752, + 7.1829, + 6.2978, + 7.8019, + 6.4897, + 4.4723, + 6.7033, + 6.2978, + 8.6492, + 7.8019, + 7.4965, + 7.9561, + 6.529, + 7.6684, + 8.6492, + 4.7885, + 7.2221, + 6.8301, + 8.4951, + 7.6684, + 6.8301, + 3.1152, + 8.1384, + 7.3055, + 7.5506, + 7.7329, + 7.1829, + 8.4951, + 7.6078, + 7.1829, + 7.1451, + 7.3499, + 7.1829, + 7.1829, + 6.0102, + 5.9412, + 7.3499, + 5.8259, + 7.4452, + 8.3615, + 6.5492, + 7.0398, + 3.3409, + 6.5698, + 5.3976, + 7.1451, + 6.1925, + 7.5506, + 8.3615, + 8.1384, + 8.0431, + 6.7274, + 6.8575, + 6.0589, + 7.6078, + 6.5908, + 7.6078, + 7.876, + 7.3965, + 8.0431, + 6.9445, + 6.6343, + 7.8019, + 5.8062, + 7.6078, + 6.5698, + 6.8301, + 6.0715, + 7.9561, + 8.1384, + 5.6874, + 8.4951, + 8.3615, + 6.6568, + 4.2105, + 6.452, + 7.4452, + 8.8315, + 6.0221, + 3.964, + 7.1088, + 8.3615, + 7.6684, + 6.9146, + 8.3615, + 6.2666, + 6.4707, + 8.0431, + 4.9603, + 7.3965, + 6.0343, + 8.2438, + 8.6492, + 7.8019, + 6.452, + 7.1088, + 5.8766, + 6.9752, + 6.5092, + 6.9146, + 7.8019, + 7.3965, + 6.3301, + 5.3976, + 8.0431, + 6.7521, + 7.6684, + 5.9192, + 7.8019, + 8.0431, + 6.9445, + 6.1369, + 7.4965, + 8.3615, + 7.6684, + 6.9752, + 8.8315, + 6.7521, + 7.7329, + 7.3499, + 7.5506, + 6.8034, + 5.9301, + 7.8019, + 5.3534, + 7.4452, + 6.8034, + 8.1384, + 7.1829, + 8.0431, + 8.0431, + 6.4897, + 6.529, + 7.0737, + 2.5264, + 4.7372, + 7.2629, + 3.0103, + 8.2438, + 6.3805, + 5.5968, + 4.48, + 4.9899, + 5.5658, + 5.5889, + 6.0972, + 6.0221, + 6.3138, + 6.5908, + 6.8301, + 7.1451, + 7.1451, + 7.5506, + 7.5506, + 7.4965, + 6.2666, + 8.0431, + 5.6788, + 7.4965 + ], + "intercepts": [ + 0.0, + 0.05, + 1.74, + -0.7, + -0.02, + 0.0, + -0.49, + 0.0, + 1.18, + 0.0, + 1.24, + 0.16, + -2.51, + -0.64 + ], + "weights": [ + [], + [ + [ + 7, + 0.05 + ], + [ + 18, + -2.27 + ], + [ + 28, + 1.76 + ], + [ + 42, + 1.78 + ], + [ + 43, + 0.41 + ], + [ + 54, + -0.6 + ], + [ + 74, + -0.85 + ], + [ + 90, + 1.07 + ], + [ + 92, + -0.17 + ], + [ + 109, + 2.13 + ], + [ + 110, + 1.14 + ], + [ + 114, + 1.28 + ], + [ + 116, + 4.53 + ], + [ + 126, + -0.07 + ], + [ + 128, + 1.39 + ], + [ + 130, + 3.57 + ], + [ + 150, + 2.33 + ], + [ + 151, + 0.48 + ], + [ + 152, + 0.28 + ], + [ + 157, + 1.44 + ], + [ + 164, + -2.54 + ], + [ + 169, + -0.16 + ], + [ + 205, + 3.61 + ], + [ + 207, + 0.28 + ], + [ + 218, + 0.18 + ], + [ + 220, + 1.75 + ], + [ + 221, + 1.12 + ], + [ + 223, + 0.77 + ], + [ + 224, + 0.86 + ], + [ + 229, + 0.14 + ], + [ + 231, + 0.83 + ], + [ + 232, + 1.85 + ], + [ + 238, + 0.04 + ], + [ + 239, + 1.23 + ], + [ + 246, + 4.3 + ], + [ + 247, + 2.44 + ], + [ + 248, + 2.32 + ], + [ + 261, + -1.12 + ], + [ + 264, + 2.01 + ], + [ + 269, + 4.41 + ], + [ + 295, + 6.26 + ], + [ + 296, + 0.42 + ], + [ + 303, + 5.63 + ], + [ + 305, + 0.55 + ], + [ + 331, + -0.8 + ], + [ + 343, + -1.24 + ], + [ + 350, + 3.12 + ], + [ + 353, + 4.13 + ], + [ + 354, + 1.76 + ], + [ + 355, + -2.16 + ], + [ + 380, + 4.16 + ], + [ + 383, + 5.15 + ], + [ + 388, + 3.39 + ], + [ + 397, + -0.05 + ], + [ + 408, + -0.68 + ], + [ + 412, + 0.31 + ], + [ + 413, + 0.09 + ], + [ + 455, + 2.87 + ], + [ + 468, + 4.99 + ], + [ + 469, + 2.42 + ], + [ + 475, + -0.53 + ], + [ + 487, + 3.77 + ], + [ + 501, + -0.04 + ], + [ + 505, + -2.48 + ], + [ + 511, + -1.2 + ], + [ + 530, + 0.38 + ], + [ + 544, + 3.48 + ], + [ + 545, + 2.29 + ], + [ + 550, + 2.76 + ], + [ + 552, + 2.97 + ], + [ + 563, + 2.27 + ], + [ + 573, + 1.8 + ], + [ + 575, + 0.07 + ], + [ + 576, + 0.73 + ], + [ + 577, + 4.02 + ], + [ + 579, + 1.65 + ], + [ + 580, + -0.44 + ], + [ + 593, + 1.34 + ], + [ + 611, + -0.2 + ], + [ + 630, + 3.43 + ], + [ + 631, + 0.94 + ], + [ + 633, + 1.51 + ], + [ + 652, + 4.09 + ], + [ + 656, + -0.42 + ], + [ + 667, + 0.95 + ], + [ + 668, + -0.6 + ], + [ + 671, + 0.74 + ], + [ + 672, + 0.31 + ], + [ + 673, + 1.55 + ], + [ + 674, + 1.26 + ], + [ + 681, + -0.41 + ], + [ + 704, + -2.28 + ], + [ + 737, + 3.19 + ], + [ + 742, + -0.21 + ], + [ + 747, + -2.78 + ], + [ + 769, + -0.56 + ], + [ + 780, + 2.7 + ], + [ + 784, + 0.06 + ], + [ + 790, + -0.8 + ], + [ + 791, + -1.31 + ], + [ + 801, + -0.24 + ], + [ + 803, + 0.38 + ], + [ + 804, + 0.27 + ], + [ + 818, + 2.67 + ], + [ + 834, + -1.16 + ], + [ + 838, + 0.18 + ], + [ + 846, + -1.63 + ], + [ + 856, + -1.33 + ], + [ + 862, + -2.64 + ], + [ + 883, + -0.14 + ], + [ + 898, + 1.19 + ], + [ + 899, + 2.72 + ], + [ + 909, + 0.24 + ], + [ + 911, + 5.04 + ], + [ + 912, + 3.41 + ], + [ + 929, + -0.2 + ], + [ + 937, + -0.03 + ], + [ + 945, + 3.26 + ], + [ + 973, + 4.82 + ], + [ + 974, + 3.74 + ], + [ + 975, + -2.6 + ], + [ + 978, + -0.27 + ], + [ + 982, + -0.32 + ], + [ + 984, + 0.08 + ], + [ + 985, + 1.23 + ], + [ + 987, + 1.73 + ], + [ + 988, + 3.09 + ], + [ + 991, + 1.2 + ], + [ + 992, + 1.2 + ], + [ + 993, + 2.36 + ], + [ + 994, + 2.36 + ], + [ + 995, + 0.03 + ], + [ + 998, + -0.06 + ] + ], + [ + [ + 4, + -0.67 + ], + [ + 7, + -0.63 + ], + [ + 9, + -3.06 + ], + [ + 18, + 4.21 + ], + [ + 22, + 0.66 + ], + [ + 24, + 1.69 + ], + [ + 26, + -0.14 + ], + [ + 29, + -1.11 + ], + [ + 31, + 0.2 + ], + [ + 36, + -0.12 + ], + [ + 48, + -0.44 + ], + [ + 51, + 0.06 + ], + [ + 61, + 1.02 + ], + [ + 67, + -1.43 + ], + [ + 69, + 1.42 + ], + [ + 74, + 1.0 + ], + [ + 75, + -0.15 + ], + [ + 79, + -0.36 + ], + [ + 81, + 0.3 + ], + [ + 83, + -1.12 + ], + [ + 86, + 2.0 + ], + [ + 88, + 2.99 + ], + [ + 89, + 0.95 + ], + [ + 92, + -0.43 + ], + [ + 94, + -3.44 + ], + [ + 105, + -2.6 + ], + [ + 108, + -0.08 + ], + [ + 109, + -0.02 + ], + [ + 112, + 1.99 + ], + [ + 118, + 1.53 + ], + [ + 119, + -0.33 + ], + [ + 120, + 2.41 + ], + [ + 124, + -0.32 + ], + [ + 126, + -1.46 + ], + [ + 131, + -0.31 + ], + [ + 134, + -2.14 + ], + [ + 145, + -0.54 + ], + [ + 146, + 2.43 + ], + [ + 147, + 2.65 + ], + [ + 150, + 1.62 + ], + [ + 156, + -1.31 + ], + [ + 159, + -0.5 + ], + [ + 160, + -1.02 + ], + [ + 162, + -1.44 + ], + [ + 164, + 0.16 + ], + [ + 167, + 1.39 + ], + [ + 168, + 2.24 + ], + [ + 172, + 0.58 + ], + [ + 179, + -1.79 + ], + [ + 184, + -2.46 + ], + [ + 189, + 2.16 + ], + [ + 191, + 0.45 + ], + [ + 194, + -0.61 + ], + [ + 196, + 0.46 + ], + [ + 198, + -1.38 + ], + [ + 199, + -0.2 + ], + [ + 212, + 1.2 + ], + [ + 213, + 0.96 + ], + [ + 214, + 1.21 + ], + [ + 215, + 1.26 + ], + [ + 216, + -0.93 + ], + [ + 221, + -0.61 + ], + [ + 222, + 2.11 + ], + [ + 225, + 1.5 + ], + [ + 226, + 2.6 + ], + [ + 229, + 1.25 + ], + [ + 230, + -1.65 + ], + [ + 231, + -1.95 + ], + [ + 246, + -0.32 + ], + [ + 248, + -0.81 + ], + [ + 251, + 1.14 + ], + [ + 261, + 0.62 + ], + [ + 263, + 1.29 + ], + [ + 265, + 2.16 + ], + [ + 266, + -0.61 + ], + [ + 270, + 0.87 + ], + [ + 279, + -0.06 + ], + [ + 295, + -1.67 + ], + [ + 303, + -0.36 + ], + [ + 304, + 0.85 + ], + [ + 305, + -0.77 + ], + [ + 308, + 1.72 + ], + [ + 310, + 2.34 + ], + [ + 312, + -1.04 + ], + [ + 327, + 1.17 + ], + [ + 328, + -0.81 + ], + [ + 331, + -0.68 + ], + [ + 348, + 1.66 + ], + [ + 350, + 0.86 + ], + [ + 351, + 1.61 + ], + [ + 355, + 0.3 + ], + [ + 356, + 2.61 + ], + [ + 361, + 1.31 + ], + [ + 362, + 0.96 + ], + [ + 363, + -0.91 + ], + [ + 369, + 0.12 + ], + [ + 370, + -0.94 + ], + [ + 373, + 0.88 + ], + [ + 374, + -0.25 + ], + [ + 375, + 1.77 + ], + [ + 378, + -1.73 + ], + [ + 380, + -0.62 + ], + [ + 382, + 0.18 + ], + [ + 385, + 0.04 + ], + [ + 386, + 1.66 + ], + [ + 387, + 2.22 + ], + [ + 388, + -0.44 + ], + [ + 390, + -0.82 + ], + [ + 391, + 1.42 + ], + [ + 392, + 3.05 + ], + [ + 393, + 0.16 + ], + [ + 395, + 1.09 + ], + [ + 397, + 5.44 + ], + [ + 398, + 0.08 + ], + [ + 399, + 0.79 + ], + [ + 403, + 1.25 + ], + [ + 405, + 2.86 + ], + [ + 408, + -1.34 + ], + [ + 409, + -1.23 + ], + [ + 419, + 0.13 + ], + [ + 420, + -0.33 + ], + [ + 423, + 0.31 + ], + [ + 424, + 0.45 + ], + [ + 425, + -0.96 + ], + [ + 426, + 0.15 + ], + [ + 428, + 3.01 + ], + [ + 429, + 3.55 + ], + [ + 430, + 0.62 + ], + [ + 434, + -1.61 + ], + [ + 442, + 0.36 + ], + [ + 443, + 0.35 + ], + [ + 447, + 0.19 + ], + [ + 448, + 0.5 + ], + [ + 450, + -0.07 + ], + [ + 457, + 3.58 + ], + [ + 458, + -2.16 + ], + [ + 459, + -0.62 + ], + [ + 465, + 1.05 + ], + [ + 467, + -0.04 + ], + [ + 470, + 3.0 + ], + [ + 471, + 1.43 + ], + [ + 473, + 1.22 + ], + [ + 474, + 1.32 + ], + [ + 475, + 0.24 + ], + [ + 486, + 2.2 + ], + [ + 488, + 1.08 + ], + [ + 491, + 0.67 + ], + [ + 494, + 1.74 + ], + [ + 498, + -0.02 + ], + [ + 500, + -1.39 + ], + [ + 501, + 0.71 + ], + [ + 503, + 0.33 + ], + [ + 504, + -1.04 + ], + [ + 505, + 1.04 + ], + [ + 510, + 0.97 + ], + [ + 514, + -0.41 + ], + [ + 515, + -0.5 + ], + [ + 518, + -1.2 + ], + [ + 520, + 0.59 + ], + [ + 529, + -0.75 + ], + [ + 532, + 0.67 + ], + [ + 534, + -0.48 + ], + [ + 536, + -1.1 + ], + [ + 537, + 1.68 + ], + [ + 538, + 1.52 + ], + [ + 540, + 0.44 + ], + [ + 542, + 1.26 + ], + [ + 543, + 0.59 + ], + [ + 546, + 0.87 + ], + [ + 550, + -0.06 + ], + [ + 570, + 1.09 + ], + [ + 571, + 1.59 + ], + [ + 572, + 0.13 + ], + [ + 576, + -0.11 + ], + [ + 581, + 2.45 + ], + [ + 582, + 2.4 + ], + [ + 583, + -1.04 + ], + [ + 587, + 0.17 + ], + [ + 588, + 2.09 + ], + [ + 590, + 1.0 + ], + [ + 591, + 0.73 + ], + [ + 592, + 1.72 + ], + [ + 593, + -0.48 + ], + [ + 594, + -1.1 + ], + [ + 603, + -0.53 + ], + [ + 608, + -0.28 + ], + [ + 611, + 0.19 + ], + [ + 612, + 1.7 + ], + [ + 621, + -0.55 + ], + [ + 623, + -0.92 + ], + [ + 624, + -0.42 + ], + [ + 630, + 1.35 + ], + [ + 632, + -0.17 + ], + [ + 636, + 1.11 + ], + [ + 637, + 0.73 + ], + [ + 638, + 1.59 + ], + [ + 639, + 0.8 + ], + [ + 642, + -1.59 + ], + [ + 644, + -0.35 + ], + [ + 647, + -1.3 + ], + [ + 648, + -0.14 + ], + [ + 652, + -0.22 + ], + [ + 656, + 3.63 + ], + [ + 657, + 2.44 + ], + [ + 659, + 0.39 + ], + [ + 661, + 0.76 + ], + [ + 667, + -0.53 + ], + [ + 669, + -1.17 + ], + [ + 671, + -2.6 + ], + [ + 677, + -0.21 + ], + [ + 679, + 0.4 + ], + [ + 680, + 1.92 + ], + [ + 683, + 1.44 + ], + [ + 697, + -0.66 + ], + [ + 698, + -1.93 + ], + [ + 699, + 0.31 + ], + [ + 700, + 2.11 + ], + [ + 701, + 2.12 + ], + [ + 704, + -1.22 + ], + [ + 708, + 1.13 + ], + [ + 711, + -1.75 + ], + [ + 719, + 2.86 + ], + [ + 721, + 4.12 + ], + [ + 723, + 2.02 + ], + [ + 730, + 1.74 + ], + [ + 733, + 0.47 + ], + [ + 734, + -0.72 + ], + [ + 745, + 0.57 + ], + [ + 746, + -1.2 + ], + [ + 747, + 0.13 + ], + [ + 749, + 1.61 + ], + [ + 750, + 0.52 + ], + [ + 759, + 0.94 + ], + [ + 761, + 2.06 + ], + [ + 763, + 0.63 + ], + [ + 767, + 1.51 + ], + [ + 768, + 1.49 + ], + [ + 769, + -2.73 + ], + [ + 770, + 1.38 + ], + [ + 775, + 2.37 + ], + [ + 776, + 1.21 + ], + [ + 777, + 2.75 + ], + [ + 783, + 0.02 + ], + [ + 784, + -0.61 + ], + [ + 785, + 0.97 + ], + [ + 786, + 1.59 + ], + [ + 787, + 1.27 + ], + [ + 789, + -0.42 + ], + [ + 790, + 3.52 + ], + [ + 791, + -1.27 + ], + [ + 797, + -0.08 + ], + [ + 798, + -1.51 + ], + [ + 807, + -0.75 + ], + [ + 810, + -0.14 + ], + [ + 817, + 1.84 + ], + [ + 818, + -2.5 + ], + [ + 820, + 0.83 + ], + [ + 821, + 0.41 + ], + [ + 825, + 2.43 + ], + [ + 826, + 0.57 + ], + [ + 829, + 2.82 + ], + [ + 830, + 0.6 + ], + [ + 834, + -0.47 + ], + [ + 836, + -1.46 + ], + [ + 843, + 0.74 + ], + [ + 845, + 0.88 + ], + [ + 846, + 1.19 + ], + [ + 848, + -2.27 + ], + [ + 856, + 1.08 + ], + [ + 861, + 1.13 + ], + [ + 862, + 0.02 + ], + [ + 866, + -0.14 + ], + [ + 867, + 0.16 + ], + [ + 878, + -0.41 + ], + [ + 881, + -1.82 + ], + [ + 892, + -0.1 + ], + [ + 895, + -1.06 + ], + [ + 896, + 1.15 + ], + [ + 901, + 0.8 + ], + [ + 903, + 1.25 + ], + [ + 904, + 0.98 + ], + [ + 906, + 2.27 + ], + [ + 907, + -1.11 + ], + [ + 908, + 1.64 + ], + [ + 911, + 0.32 + ], + [ + 920, + 2.33 + ], + [ + 927, + 0.45 + ], + [ + 935, + 0.86 + ], + [ + 937, + 0.14 + ], + [ + 939, + 0.46 + ], + [ + 941, + 0.86 + ], + [ + 943, + 1.31 + ], + [ + 944, + 0.13 + ], + [ + 947, + 0.32 + ], + [ + 948, + 2.53 + ], + [ + 949, + 1.33 + ], + [ + 951, + 2.08 + ], + [ + 955, + -1.77 + ], + [ + 956, + -1.0 + ], + [ + 960, + 0.74 + ], + [ + 961, + -0.67 + ], + [ + 963, + -0.24 + ], + [ + 972, + -2.58 + ], + [ + 973, + -0.14 + ], + [ + 974, + -2.58 + ], + [ + 975, + 1.38 + ], + [ + 983, + 4.35 + ], + [ + 985, + 3.47 + ], + [ + 986, + 0.01 + ], + [ + 987, + 0.39 + ], + [ + 990, + -0.62 + ], + [ + 991, + 0.4 + ], + [ + 992, + 0.4 + ], + [ + 995, + 0.78 + ], + [ + 996, + 0.49 + ], + [ + 998, + -1.96 + ] + ], + [ + [ + 5, + 0.49 + ], + [ + 18, + 0.58 + ], + [ + 35, + 3.01 + ], + [ + 36, + 0.3 + ], + [ + 79, + 3.66 + ], + [ + 80, + 10.99 + ], + [ + 93, + 0.76 + ], + [ + 104, + 8.71 + ], + [ + 112, + 0.88 + ], + [ + 164, + -1.17 + ], + [ + 165, + 0.28 + ], + [ + 167, + 2.82 + ], + [ + 197, + 0.82 + ], + [ + 240, + 1.05 + ], + [ + 263, + 1.38 + ], + [ + 265, + 1.75 + ], + [ + 272, + 18.11 + ], + [ + 279, + 13.46 + ], + [ + 282, + 16.59 + ], + [ + 290, + -0.04 + ], + [ + 293, + 1.7 + ], + [ + 294, + 8.73 + ], + [ + 318, + -3.9 + ], + [ + 348, + 2.2 + ], + [ + 355, + -0.22 + ], + [ + 371, + 0.11 + ], + [ + 390, + 0.41 + ], + [ + 396, + 0.21 + ], + [ + 397, + 4.21 + ], + [ + 403, + 2.27 + ], + [ + 413, + -0.59 + ], + [ + 445, + 13.26 + ], + [ + 446, + 1.58 + ], + [ + 447, + -1.09 + ], + [ + 456, + 1.36 + ], + [ + 461, + 2.62 + ], + [ + 491, + 2.22 + ], + [ + 505, + 1.12 + ], + [ + 518, + 11.99 + ], + [ + 525, + 9.21 + ], + [ + 526, + 2.23 + ], + [ + 554, + -2.01 + ], + [ + 564, + 0.6 + ], + [ + 593, + 0.54 + ], + [ + 598, + 0.3 + ], + [ + 611, + 0.59 + ], + [ + 621, + 2.28 + ], + [ + 622, + 0.39 + ], + [ + 643, + 7.03 + ], + [ + 666, + -1.48 + ], + [ + 675, + 0.85 + ], + [ + 677, + 2.38 + ], + [ + 728, + 8.84 + ], + [ + 737, + 1.97 + ], + [ + 747, + -1.63 + ], + [ + 766, + 6.69 + ], + [ + 789, + -0.33 + ], + [ + 790, + 2.42 + ], + [ + 810, + 2.48 + ], + [ + 812, + 0.02 + ], + [ + 835, + -0.02 + ], + [ + 836, + -0.74 + ], + [ + 842, + 1.26 + ], + [ + 856, + 0.06 + ], + [ + 864, + 1.59 + ], + [ + 883, + 1.17 + ], + [ + 884, + 1.23 + ], + [ + 920, + 2.42 + ], + [ + 926, + 0.41 + ], + [ + 931, + 13.38 + ], + [ + 934, + 6.13 + ], + [ + 960, + 2.25 + ], + [ + 998, + 0.12 + ] + ], + [ + [ + 9, + -0.26 + ], + [ + 18, + 0.16 + ], + [ + 35, + -0.7 + ], + [ + 43, + -0.88 + ], + [ + 44, + 1.14 + ], + [ + 67, + 1.07 + ], + [ + 100, + 2.67 + ], + [ + 101, + -0.32 + ], + [ + 102, + 0.79 + ], + [ + 103, + 2.19 + ], + [ + 147, + 0.37 + ], + [ + 149, + 1.74 + ], + [ + 156, + 1.4 + ], + [ + 164, + -0.36 + ], + [ + 165, + 2.16 + ], + [ + 204, + 0.14 + ], + [ + 214, + 0.27 + ], + [ + 218, + 1.26 + ], + [ + 227, + 0.15 + ], + [ + 231, + 0.86 + ], + [ + 240, + 1.91 + ], + [ + 241, + 2.41 + ], + [ + 261, + 0.05 + ], + [ + 312, + 2.04 + ], + [ + 314, + -0.23 + ], + [ + 328, + 2.6 + ], + [ + 330, + 2.71 + ], + [ + 331, + 4.31 + ], + [ + 332, + 1.33 + ], + [ + 333, + 0.98 + ], + [ + 334, + 5.99 + ], + [ + 335, + 2.31 + ], + [ + 336, + 1.65 + ], + [ + 337, + 2.17 + ], + [ + 338, + 1.62 + ], + [ + 339, + 6.64 + ], + [ + 341, + 2.63 + ], + [ + 342, + 0.58 + ], + [ + 382, + 0.27 + ], + [ + 393, + 0.07 + ], + [ + 407, + 2.02 + ], + [ + 413, + 0.69 + ], + [ + 419, + 0.99 + ], + [ + 420, + 0.51 + ], + [ + 423, + 0.94 + ], + [ + 439, + 0.33 + ], + [ + 447, + -0.36 + ], + [ + 459, + 0.82 + ], + [ + 472, + 1.89 + ], + [ + 475, + -1.31 + ], + [ + 485, + 2.29 + ], + [ + 505, + -0.65 + ], + [ + 534, + 0.65 + ], + [ + 535, + 0.62 + ], + [ + 536, + 0.03 + ], + [ + 551, + 1.31 + ], + [ + 586, + 2.62 + ], + [ + 593, + -0.4 + ], + [ + 599, + 2.0 + ], + [ + 624, + 0.63 + ], + [ + 630, + 0.69 + ], + [ + 645, + -0.15 + ], + [ + 666, + 1.35 + ], + [ + 667, + 0.54 + ], + [ + 668, + 2.52 + ], + [ + 670, + 4.25 + ], + [ + 671, + 0.79 + ], + [ + 672, + 0.55 + ], + [ + 674, + 0.69 + ], + [ + 698, + 1.56 + ], + [ + 720, + 2.32 + ], + [ + 734, + 3.07 + ], + [ + 747, + -1.75 + ], + [ + 764, + 0.62 + ], + [ + 765, + 1.77 + ], + [ + 786, + 0.4 + ], + [ + 791, + 1.14 + ], + [ + 795, + 3.58 + ], + [ + 797, + 2.75 + ], + [ + 798, + 0.85 + ], + [ + 817, + 0.13 + ], + [ + 834, + 0.73 + ], + [ + 850, + 4.15 + ], + [ + 866, + 0.32 + ], + [ + 885, + 0.65 + ], + [ + 920, + 2.56 + ], + [ + 922, + 0.19 + ], + [ + 924, + 0.44 + ], + [ + 929, + 0.96 + ], + [ + 937, + 0.15 + ], + [ + 945, + 3.55 + ], + [ + 946, + 0.68 + ], + [ + 961, + 0.33 + ], + [ + 970, + 0.31 + ], + [ + 971, + 0.31 + ], + [ + 972, + 2.79 + ], + [ + 982, + 0.94 + ], + [ + 983, + 0.37 + ], + [ + 989, + 1.09 + ] + ], + [], + [ + [ + 10, + 1.07 + ], + [ + 17, + 7.29 + ], + [ + 18, + -1.84 + ], + [ + 29, + 0.63 + ], + [ + 35, + -0.95 + ], + [ + 43, + -0.23 + ], + [ + 47, + 2.09 + ], + [ + 54, + 3.06 + ], + [ + 58, + 1.46 + ], + [ + 74, + -0.62 + ], + [ + 77, + -0.25 + ], + [ + 83, + 1.72 + ], + [ + 88, + 3.11 + ], + [ + 92, + 0.11 + ], + [ + 94, + 1.95 + ], + [ + 95, + 0.22 + ], + [ + 97, + 0.66 + ], + [ + 126, + 4.31 + ], + [ + 127, + 3.89 + ], + [ + 136, + 8.39 + ], + [ + 138, + 13.11 + ], + [ + 143, + 0.74 + ], + [ + 150, + -2.02 + ], + [ + 169, + 5.17 + ], + [ + 170, + 0.77 + ], + [ + 171, + 0.07 + ], + [ + 173, + 2.27 + ], + [ + 175, + 2.66 + ], + [ + 178, + 1.05 + ], + [ + 179, + 1.51 + ], + [ + 181, + 0.91 + ], + [ + 183, + 0.9 + ], + [ + 184, + 2.09 + ], + [ + 192, + -0.04 + ], + [ + 216, + 0.25 + ], + [ + 219, + 0.05 + ], + [ + 228, + 0.86 + ], + [ + 252, + -2.21 + ], + [ + 260, + -0.09 + ], + [ + 264, + 0.36 + ], + [ + 270, + 0.15 + ], + [ + 305, + 1.22 + ], + [ + 307, + 0.23 + ], + [ + 309, + 1.82 + ], + [ + 314, + 3.22 + ], + [ + 316, + 6.92 + ], + [ + 318, + 18.77 + ], + [ + 325, + 7.63 + ], + [ + 343, + 0.5 + ], + [ + 355, + 0.43 + ], + [ + 359, + 1.01 + ], + [ + 365, + 4.45 + ], + [ + 366, + 1.56 + ], + [ + 374, + 1.54 + ], + [ + 388, + 0.87 + ], + [ + 409, + 1.97 + ], + [ + 413, + -1.63 + ], + [ + 423, + -0.22 + ], + [ + 430, + 0.04 + ], + [ + 447, + -0.24 + ], + [ + 475, + 1.37 + ], + [ + 494, + 0.08 + ], + [ + 500, + 2.76 + ], + [ + 515, + 0.26 + ], + [ + 517, + 1.76 + ], + [ + 522, + 5.49 + ], + [ + 524, + 2.94 + ], + [ + 528, + 1.03 + ], + [ + 556, + 18.95 + ], + [ + 562, + 0.39 + ], + [ + 580, + -0.29 + ], + [ + 583, + 0.69 + ], + [ + 597, + 2.4 + ], + [ + 603, + -1.86 + ], + [ + 607, + 1.47 + ], + [ + 610, + 1.43 + ], + [ + 611, + -0.45 + ], + [ + 630, + -0.06 + ], + [ + 632, + 0.45 + ], + [ + 633, + 1.08 + ], + [ + 642, + 0.43 + ], + [ + 645, + 1.38 + ], + [ + 647, + 0.39 + ], + [ + 663, + -0.59 + ], + [ + 666, + -0.52 + ], + [ + 668, + -1.38 + ], + [ + 681, + 2.65 + ], + [ + 686, + 0.58 + ], + [ + 696, + -0.38 + ], + [ + 709, + 0.83 + ], + [ + 711, + 0.25 + ], + [ + 713, + 0.06 + ], + [ + 715, + 2.56 + ], + [ + 716, + 2.56 + ], + [ + 723, + 0.28 + ], + [ + 731, + 1.29 + ], + [ + 732, + 0.58 + ], + [ + 789, + 1.71 + ], + [ + 790, + -0.58 + ], + [ + 791, + 0.37 + ], + [ + 801, + 0.94 + ], + [ + 807, + 3.23 + ], + [ + 827, + 5.11 + ], + [ + 832, + 2.92 + ], + [ + 835, + 0.21 + ], + [ + 836, + 0.91 + ], + [ + 840, + 3.47 + ], + [ + 846, + -1.21 + ], + [ + 862, + -4.09 + ], + [ + 883, + 0.76 + ], + [ + 885, + 1.52 + ], + [ + 886, + 0.37 + ], + [ + 902, + 0.47 + ], + [ + 904, + -0.4 + ], + [ + 911, + -0.32 + ], + [ + 919, + 0.96 + ], + [ + 920, + -0.16 + ], + [ + 929, + 0.21 + ], + [ + 936, + 0.03 + ], + [ + 963, + -0.19 + ], + [ + 965, + 4.23 + ], + [ + 973, + 2.67 + ], + [ + 975, + 2.25 + ], + [ + 976, + 3.21 + ], + [ + 977, + 2.8 + ], + [ + 978, + -1.26 + ], + [ + 979, + 4.17 + ], + [ + 990, + 2.7 + ] + ], + [], + [ + [ + 2, + 0.81 + ], + [ + 3, + 3.87 + ], + [ + 4, + 0.14 + ], + [ + 7, + -0.82 + ], + [ + 9, + 0.72 + ], + [ + 10, + -0.35 + ], + [ + 11, + 0.32 + ], + [ + 12, + -0.89 + ], + [ + 15, + -0.16 + ], + [ + 16, + 0.65 + ], + [ + 17, + 10.45 + ], + [ + 18, + -0.45 + ], + [ + 26, + -0.04 + ], + [ + 41, + -1.18 + ], + [ + 43, + -0.33 + ], + [ + 47, + -0.56 + ], + [ + 48, + 0.09 + ], + [ + 62, + 0.73 + ], + [ + 72, + 10.9 + ], + [ + 74, + -0.59 + ], + [ + 81, + -0.27 + ], + [ + 86, + -1.41 + ], + [ + 88, + 1.42 + ], + [ + 91, + 0.64 + ], + [ + 92, + -0.75 + ], + [ + 95, + 0.49 + ], + [ + 97, + 0.11 + ], + [ + 105, + 1.83 + ], + [ + 119, + -0.05 + ], + [ + 126, + 1.92 + ], + [ + 127, + 1.46 + ], + [ + 136, + 10.77 + ], + [ + 137, + 0.98 + ], + [ + 138, + 15.92 + ], + [ + 139, + 0.22 + ], + [ + 144, + 6.88 + ], + [ + 145, + 1.71 + ], + [ + 147, + -0.71 + ], + [ + 154, + 1.0 + ], + [ + 160, + 0.04 + ], + [ + 164, + -0.51 + ], + [ + 165, + -0.71 + ], + [ + 169, + -1.28 + ], + [ + 171, + -2.59 + ], + [ + 180, + -0.59 + ], + [ + 192, + 2.64 + ], + [ + 193, + 0.26 + ], + [ + 194, + 0.37 + ], + [ + 198, + 0.04 + ], + [ + 200, + 0.84 + ], + [ + 202, + 2.85 + ], + [ + 204, + 1.54 + ], + [ + 218, + -1.59 + ], + [ + 220, + -0.23 + ], + [ + 221, + -1.51 + ], + [ + 223, + -0.86 + ], + [ + 228, + -1.12 + ], + [ + 234, + 0.13 + ], + [ + 237, + 0.43 + ], + [ + 240, + -0.36 + ], + [ + 248, + -0.02 + ], + [ + 252, + 1.34 + ], + [ + 253, + 1.11 + ], + [ + 254, + 0.92 + ], + [ + 259, + 0.86 + ], + [ + 260, + 0.08 + ], + [ + 264, + 0.4 + ], + [ + 266, + 2.06 + ], + [ + 282, + 3.5 + ], + [ + 295, + 1.12 + ], + [ + 296, + -0.06 + ], + [ + 305, + 0.84 + ], + [ + 311, + 0.17 + ], + [ + 314, + 4.54 + ], + [ + 315, + 0.73 + ], + [ + 316, + 6.82 + ], + [ + 317, + 5.52 + ], + [ + 318, + 21.11 + ], + [ + 319, + 0.35 + ], + [ + 322, + 0.56 + ], + [ + 324, + 0.11 + ], + [ + 325, + 10.45 + ], + [ + 328, + -0.25 + ], + [ + 329, + 0.26 + ], + [ + 343, + 0.7 + ], + [ + 349, + 1.47 + ], + [ + 359, + 1.29 + ], + [ + 361, + -1.3 + ], + [ + 362, + -0.32 + ], + [ + 363, + 0.33 + ], + [ + 365, + 6.88 + ], + [ + 366, + 6.57 + ], + [ + 367, + 2.72 + ], + [ + 369, + -0.24 + ], + [ + 371, + -0.64 + ], + [ + 374, + -2.41 + ], + [ + 376, + 4.96 + ], + [ + 378, + 0.3 + ], + [ + 380, + 0.52 + ], + [ + 385, + 0.3 + ], + [ + 390, + -0.32 + ], + [ + 395, + -0.03 + ], + [ + 397, + 0.82 + ], + [ + 408, + 0.43 + ], + [ + 413, + -0.82 + ], + [ + 415, + -0.75 + ], + [ + 430, + -1.26 + ], + [ + 434, + -1.13 + ], + [ + 435, + 0.13 + ], + [ + 437, + 1.36 + ], + [ + 444, + 5.15 + ], + [ + 445, + 0.24 + ], + [ + 447, + 1.41 + ], + [ + 448, + 0.13 + ], + [ + 459, + 2.43 + ], + [ + 464, + 0.22 + ], + [ + 467, + 0.59 + ], + [ + 471, + 0.02 + ], + [ + 475, + -0.97 + ], + [ + 481, + -0.17 + ], + [ + 495, + 1.53 + ], + [ + 498, + 0.57 + ], + [ + 499, + 4.01 + ], + [ + 500, + 0.82 + ], + [ + 510, + -0.1 + ], + [ + 511, + -0.32 + ], + [ + 518, + 0.91 + ], + [ + 522, + 2.94 + ], + [ + 523, + 3.62 + ], + [ + 524, + 5.57 + ], + [ + 528, + 1.08 + ], + [ + 538, + 1.24 + ], + [ + 543, + -1.14 + ], + [ + 547, + 0.63 + ], + [ + 550, + -0.3 + ], + [ + 556, + 19.44 + ], + [ + 559, + 0.03 + ], + [ + 564, + 0.77 + ], + [ + 571, + 0.15 + ], + [ + 583, + 1.04 + ], + [ + 594, + 1.0 + ], + [ + 598, + -0.52 + ], + [ + 599, + 0.35 + ], + [ + 603, + -0.44 + ], + [ + 607, + 0.8 + ], + [ + 610, + -0.45 + ], + [ + 611, + 0.94 + ], + [ + 622, + -0.15 + ], + [ + 624, + 2.36 + ], + [ + 630, + -0.53 + ], + [ + 638, + 0.37 + ], + [ + 641, + 1.4 + ], + [ + 642, + 1.22 + ], + [ + 644, + 2.74 + ], + [ + 645, + 3.66 + ], + [ + 648, + 1.32 + ], + [ + 649, + 2.81 + ], + [ + 650, + 2.32 + ], + [ + 656, + 0.18 + ], + [ + 663, + 0.22 + ], + [ + 666, + 0.21 + ], + [ + 667, + -0.48 + ], + [ + 668, + 0.23 + ], + [ + 671, + 0.75 + ], + [ + 675, + -1.12 + ], + [ + 681, + 1.15 + ], + [ + 695, + 1.46 + ], + [ + 702, + 0.06 + ], + [ + 703, + -0.75 + ], + [ + 704, + 0.45 + ], + [ + 710, + -1.19 + ], + [ + 711, + 0.21 + ], + [ + 713, + 2.31 + ], + [ + 721, + 0.77 + ], + [ + 723, + 1.6 + ], + [ + 724, + 0.03 + ], + [ + 728, + 2.24 + ], + [ + 737, + -0.86 + ], + [ + 746, + 0.9 + ], + [ + 747, + 0.59 + ], + [ + 758, + 0.46 + ], + [ + 776, + 0.36 + ], + [ + 779, + 1.11 + ], + [ + 784, + -0.06 + ], + [ + 785, + -0.08 + ], + [ + 789, + 2.9 + ], + [ + 799, + 0.61 + ], + [ + 801, + -0.2 + ], + [ + 807, + 0.69 + ], + [ + 808, + 2.3 + ], + [ + 809, + 1.88 + ], + [ + 813, + 3.12 + ], + [ + 814, + 3.12 + ], + [ + 815, + 5.11 + ], + [ + 817, + -0.28 + ], + [ + 818, + -0.84 + ], + [ + 820, + -0.44 + ], + [ + 824, + -0.98 + ], + [ + 826, + -0.22 + ], + [ + 827, + 5.78 + ], + [ + 828, + 1.24 + ], + [ + 832, + 1.44 + ], + [ + 834, + 0.21 + ], + [ + 836, + 0.81 + ], + [ + 843, + 0.41 + ], + [ + 846, + 0.1 + ], + [ + 862, + -1.12 + ], + [ + 871, + 0.71 + ], + [ + 878, + 0.44 + ], + [ + 881, + 1.82 + ], + [ + 883, + -0.56 + ], + [ + 887, + 1.16 + ], + [ + 892, + 0.53 + ], + [ + 902, + -0.02 + ], + [ + 907, + 0.7 + ], + [ + 915, + 0.38 + ], + [ + 916, + 0.02 + ], + [ + 920, + -0.54 + ], + [ + 927, + -0.4 + ], + [ + 929, + -0.48 + ], + [ + 942, + -0.49 + ], + [ + 944, + -0.72 + ], + [ + 952, + 0.4 + ], + [ + 963, + -0.26 + ], + [ + 965, + 2.66 + ], + [ + 969, + 0.61 + ], + [ + 976, + -2.82 + ], + [ + 978, + 1.71 + ], + [ + 980, + 1.23 + ], + [ + 982, + 0.35 + ], + [ + 983, + -0.45 + ], + [ + 987, + -0.08 + ], + [ + 988, + -1.11 + ], + [ + 990, + 1.77 + ], + [ + 998, + 0.3 + ] + ], + [], + [ + [ + 0, + -0.32 + ], + [ + 2, + 0.94 + ], + [ + 4, + -0.97 + ], + [ + 9, + 0.81 + ], + [ + 11, + -0.44 + ], + [ + 12, + 0.43 + ], + [ + 17, + -1.65 + ], + [ + 18, + 1.82 + ], + [ + 31, + -1.77 + ], + [ + 32, + 0.28 + ], + [ + 35, + 0.58 + ], + [ + 43, + -0.57 + ], + [ + 44, + -0.07 + ], + [ + 47, + 2.05 + ], + [ + 56, + 2.11 + ], + [ + 59, + 0.22 + ], + [ + 64, + 0.03 + ], + [ + 65, + 0.46 + ], + [ + 67, + -0.04 + ], + [ + 74, + -1.83 + ], + [ + 77, + 0.27 + ], + [ + 83, + 2.06 + ], + [ + 85, + 3.31 + ], + [ + 86, + -0.21 + ], + [ + 88, + -1.67 + ], + [ + 94, + 0.36 + ], + [ + 95, + -0.23 + ], + [ + 96, + 1.05 + ], + [ + 102, + 1.44 + ], + [ + 105, + 3.7 + ], + [ + 106, + 1.13 + ], + [ + 108, + 1.7 + ], + [ + 109, + 0.64 + ], + [ + 117, + 0.62 + ], + [ + 119, + 0.34 + ], + [ + 120, + -1.08 + ], + [ + 123, + 0.43 + ], + [ + 126, + 1.46 + ], + [ + 127, + -1.94 + ], + [ + 129, + 2.64 + ], + [ + 145, + 0.92 + ], + [ + 147, + -1.06 + ], + [ + 148, + 2.2 + ], + [ + 150, + -1.57 + ], + [ + 156, + 2.7 + ], + [ + 157, + -0.55 + ], + [ + 160, + -1.36 + ], + [ + 164, + 0.96 + ], + [ + 165, + 1.68 + ], + [ + 167, + -0.4 + ], + [ + 169, + 1.77 + ], + [ + 170, + 1.25 + ], + [ + 180, + 0.51 + ], + [ + 185, + 0.96 + ], + [ + 186, + 3.18 + ], + [ + 187, + -1.0 + ], + [ + 189, + -0.61 + ], + [ + 192, + 0.83 + ], + [ + 204, + -1.44 + ], + [ + 214, + 0.6 + ], + [ + 216, + 2.52 + ], + [ + 221, + -0.31 + ], + [ + 223, + -2.72 + ], + [ + 225, + -0.23 + ], + [ + 227, + -0.65 + ], + [ + 228, + -0.31 + ], + [ + 231, + -0.6 + ], + [ + 234, + 0.33 + ], + [ + 240, + 4.02 + ], + [ + 242, + 0.36 + ], + [ + 243, + 1.85 + ], + [ + 248, + 0.31 + ], + [ + 249, + 0.66 + ], + [ + 251, + -1.63 + ], + [ + 252, + -5.91 + ], + [ + 260, + -1.05 + ], + [ + 261, + -0.65 + ], + [ + 264, + -1.15 + ], + [ + 266, + 0.94 + ], + [ + 267, + 4.02 + ], + [ + 268, + 0.51 + ], + [ + 270, + -0.52 + ], + [ + 271, + -0.06 + ], + [ + 282, + -6.18 + ], + [ + 295, + -0.61 + ], + [ + 305, + -0.92 + ], + [ + 307, + -0.05 + ], + [ + 314, + 2.65 + ], + [ + 315, + -0.15 + ], + [ + 327, + -1.61 + ], + [ + 328, + 0.41 + ], + [ + 329, + -2.05 + ], + [ + 331, + 0.94 + ], + [ + 341, + 2.05 + ], + [ + 343, + 0.95 + ], + [ + 355, + 1.2 + ], + [ + 357, + 0.55 + ], + [ + 358, + 1.68 + ], + [ + 361, + -0.09 + ], + [ + 367, + 0.99 + ], + [ + 368, + -0.27 + ], + [ + 370, + -0.33 + ], + [ + 371, + 0.32 + ], + [ + 373, + -1.36 + ], + [ + 378, + 1.32 + ], + [ + 379, + 0.17 + ], + [ + 380, + -1.06 + ], + [ + 385, + -0.06 + ], + [ + 390, + 0.08 + ], + [ + 391, + -0.26 + ], + [ + 393, + 0.93 + ], + [ + 408, + -0.17 + ], + [ + 409, + 5.06 + ], + [ + 415, + 2.3 + ], + [ + 416, + 0.77 + ], + [ + 419, + -0.98 + ], + [ + 420, + 1.52 + ], + [ + 423, + 1.58 + ], + [ + 425, + 0.34 + ], + [ + 427, + 1.03 + ], + [ + 430, + -1.04 + ], + [ + 432, + 2.14 + ], + [ + 434, + 0.66 + ], + [ + 447, + 0.38 + ], + [ + 452, + 2.4 + ], + [ + 458, + 3.2 + ], + [ + 459, + -0.01 + ], + [ + 471, + -1.79 + ], + [ + 475, + 0.3 + ], + [ + 478, + 1.15 + ], + [ + 479, + 1.35 + ], + [ + 481, + 0.13 + ], + [ + 482, + 1.06 + ], + [ + 484, + 0.33 + ], + [ + 494, + -1.91 + ], + [ + 502, + 1.62 + ], + [ + 504, + 1.31 + ], + [ + 505, + -0.28 + ], + [ + 511, + 0.38 + ], + [ + 512, + 0.57 + ], + [ + 514, + -0.52 + ], + [ + 516, + 0.69 + ], + [ + 529, + 0.31 + ], + [ + 532, + 0.74 + ], + [ + 533, + 0.45 + ], + [ + 534, + 1.12 + ], + [ + 536, + 2.8 + ], + [ + 538, + -0.19 + ], + [ + 542, + -0.78 + ], + [ + 547, + 2.08 + ], + [ + 551, + 1.29 + ], + [ + 567, + 1.18 + ], + [ + 571, + -0.74 + ], + [ + 580, + -1.81 + ], + [ + 599, + 1.27 + ], + [ + 602, + -0.54 + ], + [ + 603, + -0.32 + ], + [ + 608, + 0.5 + ], + [ + 610, + 1.49 + ], + [ + 611, + -1.21 + ], + [ + 614, + 1.01 + ], + [ + 623, + 0.85 + ], + [ + 624, + 2.85 + ], + [ + 625, + 0.84 + ], + [ + 626, + 0.48 + ], + [ + 627, + 0.79 + ], + [ + 628, + 3.06 + ], + [ + 644, + 2.39 + ], + [ + 645, + 1.9 + ], + [ + 647, + -0.88 + ], + [ + 648, + 2.17 + ], + [ + 650, + 1.45 + ], + [ + 652, + 0.93 + ], + [ + 654, + 2.2 + ], + [ + 656, + -1.0 + ], + [ + 663, + -1.06 + ], + [ + 668, + 0.67 + ], + [ + 669, + 0.1 + ], + [ + 681, + 1.1 + ], + [ + 684, + 0.65 + ], + [ + 692, + 0.79 + ], + [ + 695, + 1.84 + ], + [ + 699, + -0.13 + ], + [ + 701, + -3.54 + ], + [ + 704, + 0.41 + ], + [ + 710, + 0.22 + ], + [ + 711, + 0.06 + ], + [ + 723, + -0.86 + ], + [ + 727, + -1.21 + ], + [ + 728, + -2.35 + ], + [ + 731, + 1.92 + ], + [ + 732, + 1.56 + ], + [ + 734, + 0.45 + ], + [ + 735, + -0.4 + ], + [ + 746, + 1.13 + ], + [ + 747, + 1.04 + ], + [ + 749, + 1.12 + ], + [ + 763, + -0.72 + ], + [ + 769, + 3.18 + ], + [ + 777, + -2.35 + ], + [ + 779, + -0.68 + ], + [ + 781, + 3.35 + ], + [ + 785, + -1.04 + ], + [ + 786, + -0.39 + ], + [ + 787, + -0.57 + ], + [ + 791, + 3.74 + ], + [ + 796, + 0.84 + ], + [ + 797, + 2.53 + ], + [ + 801, + 1.45 + ], + [ + 809, + 1.65 + ], + [ + 810, + 1.12 + ], + [ + 818, + -2.4 + ], + [ + 820, + 0.29 + ], + [ + 827, + -1.35 + ], + [ + 834, + 1.67 + ], + [ + 835, + -1.71 + ], + [ + 836, + 0.59 + ], + [ + 842, + -0.5 + ], + [ + 846, + 0.86 + ], + [ + 862, + -1.8 + ], + [ + 876, + 2.97 + ], + [ + 878, + 2.39 + ], + [ + 881, + 1.08 + ], + [ + 882, + 4.72 + ], + [ + 883, + 0.07 + ], + [ + 886, + 1.67 + ], + [ + 887, + 0.06 + ], + [ + 890, + 0.5 + ], + [ + 892, + 0.74 + ], + [ + 893, + 0.46 + ], + [ + 900, + 2.36 + ], + [ + 906, + -0.41 + ], + [ + 907, + 0.52 + ], + [ + 911, + -0.49 + ], + [ + 920, + 3.0 + ], + [ + 929, + 1.9 + ], + [ + 937, + 2.0 + ], + [ + 938, + -0.83 + ], + [ + 943, + -0.37 + ], + [ + 944, + -0.71 + ], + [ + 946, + 1.66 + ], + [ + 955, + 2.09 + ], + [ + 958, + 2.93 + ], + [ + 960, + -0.17 + ], + [ + 972, + 1.04 + ], + [ + 975, + -0.26 + ], + [ + 976, + 1.36 + ], + [ + 977, + -1.27 + ], + [ + 978, + -0.39 + ], + [ + 982, + 1.25 + ], + [ + 983, + 1.05 + ], + [ + 985, + -2.2 + ], + [ + 986, + -0.55 + ], + [ + 989, + 1.0 + ], + [ + 990, + 0.05 + ], + [ + 995, + -0.41 + ], + [ + 998, + 0.12 + ] + ], + [ + [ + 29, + 1.88 + ], + [ + 35, + -0.62 + ], + [ + 41, + 0.05 + ], + [ + 43, + 0.58 + ], + [ + 92, + 1.82 + ], + [ + 97, + -0.23 + ], + [ + 99, + 1.44 + ], + [ + 101, + 0.2 + ], + [ + 105, + -0.62 + ], + [ + 126, + -0.44 + ], + [ + 153, + 0.23 + ], + [ + 159, + 2.07 + ], + [ + 187, + 1.2 + ], + [ + 188, + 2.14 + ], + [ + 189, + 0.98 + ], + [ + 192, + -0.77 + ], + [ + 223, + 0.6 + ], + [ + 229, + 0.46 + ], + [ + 237, + 2.64 + ], + [ + 240, + -0.25 + ], + [ + 271, + 1.5 + ], + [ + 295, + 1.3 + ], + [ + 302, + 1.67 + ], + [ + 313, + 1.23 + ], + [ + 314, + -0.65 + ], + [ + 327, + 1.08 + ], + [ + 329, + 0.18 + ], + [ + 331, + 0.88 + ], + [ + 343, + -0.43 + ], + [ + 352, + 1.2 + ], + [ + 355, + -0.95 + ], + [ + 370, + 2.99 + ], + [ + 371, + 2.77 + ], + [ + 372, + 2.65 + ], + [ + 393, + -0.49 + ], + [ + 394, + 1.04 + ], + [ + 397, + -0.62 + ], + [ + 413, + 0.05 + ], + [ + 419, + 0.29 + ], + [ + 434, + 0.34 + ], + [ + 459, + 4.69 + ], + [ + 468, + 1.54 + ], + [ + 489, + 1.26 + ], + [ + 490, + 2.99 + ], + [ + 501, + 2.59 + ], + [ + 511, + -0.42 + ], + [ + 513, + 4.14 + ], + [ + 546, + 2.11 + ], + [ + 569, + 1.93 + ], + [ + 580, + 1.04 + ], + [ + 582, + 2.43 + ], + [ + 593, + -0.59 + ], + [ + 603, + 0.68 + ], + [ + 618, + 0.76 + ], + [ + 647, + 1.33 + ], + [ + 655, + 2.18 + ], + [ + 656, + -0.79 + ], + [ + 664, + 2.29 + ], + [ + 666, + 1.05 + ], + [ + 667, + 1.5 + ], + [ + 668, + 0.63 + ], + [ + 681, + -0.67 + ], + [ + 698, + -0.35 + ], + [ + 699, + 0.71 + ], + [ + 717, + 3.59 + ], + [ + 733, + 0.16 + ], + [ + 734, + -0.86 + ], + [ + 737, + 7.1 + ], + [ + 738, + 2.11 + ], + [ + 739, + 0.45 + ], + [ + 741, + 1.85 + ], + [ + 742, + 2.44 + ], + [ + 743, + 6.26 + ], + [ + 744, + 3.29 + ], + [ + 784, + 1.02 + ], + [ + 790, + 1.61 + ], + [ + 791, + -1.28 + ], + [ + 801, + 0.4 + ], + [ + 806, + 1.75 + ], + [ + 818, + 1.29 + ], + [ + 823, + 3.12 + ], + [ + 824, + 4.15 + ], + [ + 835, + 0.69 + ], + [ + 836, + -1.13 + ], + [ + 844, + 0.55 + ], + [ + 845, + 1.05 + ], + [ + 846, + -0.21 + ], + [ + 861, + 1.17 + ], + [ + 875, + 1.0 + ], + [ + 883, + -1.91 + ], + [ + 909, + 1.61 + ], + [ + 911, + 0.44 + ], + [ + 920, + -1.28 + ], + [ + 929, + -1.29 + ], + [ + 940, + 2.27 + ], + [ + 943, + 1.13 + ], + [ + 956, + 0.18 + ], + [ + 963, + 0.65 + ], + [ + 970, + 1.26 + ], + [ + 971, + 1.26 + ], + [ + 982, + -0.91 + ], + [ + 983, + -0.13 + ], + [ + 986, + 0.13 + ] + ], + [ + [ + 190, + 8.37 + ], + [ + 436, + 2.62 + ], + [ + 462, + 2.62 + ], + [ + 656, + 3.98 + ], + [ + 662, + 27.29 + ], + [ + 701, + 7.13 + ], + [ + 702, + 1.13 + ], + [ + 736, + 8.71 + ], + [ + 747, + 2.27 + ], + [ + 748, + 1.82 + ], + [ + 759, + 4.62 + ], + [ + 760, + 4.42 + ], + [ + 872, + 4.19 + ], + [ + 978, + 1.02 + ] + ], + [ + [ + 9, + -0.73 + ], + [ + 26, + 0.95 + ], + [ + 31, + 0.52 + ], + [ + 34, + 0.2 + ], + [ + 35, + 2.9 + ], + [ + 67, + 1.01 + ], + [ + 74, + 0.09 + ], + [ + 87, + 1.86 + ], + [ + 91, + 0.24 + ], + [ + 117, + 2.32 + ], + [ + 119, + 2.97 + ], + [ + 122, + 0.75 + ], + [ + 134, + 6.04 + ], + [ + 150, + 0.92 + ], + [ + 161, + 1.41 + ], + [ + 162, + 0.38 + ], + [ + 164, + 1.38 + ], + [ + 189, + 0.83 + ], + [ + 192, + 1.32 + ], + [ + 203, + 0.71 + ], + [ + 221, + 1.52 + ], + [ + 231, + 0.75 + ], + [ + 233, + 2.56 + ], + [ + 248, + 0.39 + ], + [ + 252, + 1.47 + ], + [ + 261, + 1.78 + ], + [ + 262, + 3.78 + ], + [ + 271, + 2.68 + ], + [ + 314, + -0.49 + ], + [ + 331, + -0.11 + ], + [ + 349, + 2.43 + ], + [ + 362, + 2.41 + ], + [ + 375, + 1.31 + ], + [ + 380, + 0.65 + ], + [ + 395, + -0.45 + ], + [ + 405, + 0.31 + ], + [ + 408, + 0.4 + ], + [ + 422, + 0.25 + ], + [ + 426, + 1.89 + ], + [ + 435, + 1.27 + ], + [ + 466, + 0.46 + ], + [ + 475, + 0.85 + ], + [ + 476, + 0.39 + ], + [ + 481, + 0.29 + ], + [ + 491, + 0.21 + ], + [ + 511, + 1.47 + ], + [ + 514, + 0.27 + ], + [ + 533, + 0.5 + ], + [ + 550, + 2.6 + ], + [ + 553, + 4.28 + ], + [ + 580, + 0.59 + ], + [ + 593, + 1.11 + ], + [ + 603, + 1.45 + ], + [ + 611, + -1.89 + ], + [ + 644, + -0.53 + ], + [ + 671, + -0.45 + ], + [ + 677, + 1.47 + ], + [ + 697, + 0.89 + ], + [ + 704, + 3.19 + ], + [ + 737, + -0.08 + ], + [ + 747, + -0.69 + ], + [ + 763, + 1.86 + ], + [ + 769, + 0.49 + ], + [ + 806, + 3.11 + ], + [ + 810, + 2.87 + ], + [ + 818, + 2.98 + ], + [ + 846, + 6.0 + ], + [ + 847, + 13.84 + ], + [ + 848, + 5.93 + ], + [ + 849, + 1.64 + ], + [ + 851, + 2.07 + ], + [ + 852, + 7.09 + ], + [ + 853, + 8.97 + ], + [ + 854, + 0.46 + ], + [ + 856, + 6.06 + ], + [ + 857, + 0.51 + ], + [ + 858, + 4.01 + ], + [ + 859, + 0.08 + ], + [ + 860, + 2.08 + ], + [ + 862, + 0.66 + ], + [ + 868, + 1.54 + ], + [ + 869, + 4.87 + ], + [ + 905, + 4.45 + ], + [ + 907, + 1.02 + ], + [ + 910, + 10.25 + ], + [ + 929, + -0.34 + ], + [ + 938, + 0.4 + ], + [ + 947, + 0.01 + ], + [ + 952, + 0.74 + ], + [ + 956, + 2.85 + ], + [ + 957, + 3.04 + ], + [ + 963, + 1.61 + ], + [ + 978, + 0.31 + ] + ] + ], + "thresholds": [ + 1.0, + 0.5, + 0.5, + 0.5, + 0.5, + 1.0, + 0.5, + 1.0, + 0.5, + 1.0, + 0.5, + 0.5, + 0.5, + 0.5 + ], + "meta": { + "trainedAt": "2026-05-30T02:38:30+00:00", + "trainSize": 12593, + "evalSize": 3149, + "evalMacroF1": 0.6434, + "evalCoverage": 0.779, + "gitCommit": "51506d9" + } +} diff --git a/src/utils/workItemClassifier/types.ts b/src/utils/workItemClassifier/types.ts new file mode 100644 index 0000000..d2e7ace --- /dev/null +++ b/src/utils/workItemClassifier/types.ts @@ -0,0 +1,25 @@ +import type { TimeEntryTypeEnum } from '#generated/types/graphql'; + +export interface WorkItemClassifierModelMeta { + trainedAt: string; + trainSize: number; + evalSize: number; + evalMacroF1: number | null; + evalCoverage: number | null; + note?: string; +} + +export interface WorkItemClassifierModel { + classes: TimeEntryTypeEnum[]; + vocabulary: string[]; + idf: number[]; + intercepts: number[]; + weights: [number, number][][]; + /** + * Per-class confidence thresholds (aligned with `classes`). The model + * returns a class only when its top-class probability meets that class's + * threshold. Each is tuned independently to maximize that class's F1. + */ + thresholds: number[]; + meta: WorkItemClassifierModelMeta; +} diff --git a/src/views/DailyJournal/index.tsx b/src/views/DailyJournal/index.tsx index 9249584..0026a86 100644 --- a/src/views/DailyJournal/index.tsx +++ b/src/views/DailyJournal/index.tsx @@ -45,7 +45,6 @@ import RouteContext from '#contexts/route'; import { MyTimeEntriesQuery, MyTimeEntriesQueryVariables, - TimeEntryTypeEnum, } from '#generated/types/graphql'; import useCommand from '#hooks/useCommand'; import { useFocusManager } from '#hooks/useFocus'; @@ -63,6 +62,7 @@ import { Task, WorkItem, } from '#utils/types'; +import inferTypeFromDescription from '#utils/workItemClassifier'; import AddWorkItemDialog from './AddWorkItemDialog'; import AvailabilityDialog from './AvailabilityDialog'; @@ -75,56 +75,6 @@ import UpdateNoteDialog from './UpdateNoteDialog'; import styles from './styles.module.css'; -function inferTypeFromDescription(desc: string): TimeEntryTypeEnum | undefined { - const lower = desc.toLowerCase(); - const matches = (pattern: RegExp) => pattern.test(lower); - - if (matches(/\b(client meeting|client call|external meeting)\b/)) { - return 'EXTERNAL_MEETING'; - } - - if ( - matches(/\b(meeting|standup|stand-up|all hands|all-hands)\b/) - || matches(/\b1:1\b/) - ) { - return 'INTERNAL_MEETING'; - } - if (matches(/\b(client discussion|external discussion)\b/)) { - return 'EXTERNAL_DISCUSSION'; - } - if (matches(/\b(discuss|discussion|brainstorm)\b/)) { - return 'INTERNAL_DISCUSSION'; - } - if (matches(/\b(review|pull request|merge request)\b/)) { - return 'REVIEW'; - } - if (matches(/\b(deploy|deployment|pipeline|ci|cd|infra|release)\b/)) { - return 'DEV_OPS'; - } - if (matches(/\b(test|tests|testing|qa|qc|regression)\b/)) { - return 'TESTING'; - } - if (matches(/\b(design|wireframe|mockup|ux|ui)\b/)) { - return 'DESIGN'; - } - if (matches(/\b(research|study|investigate|spike|explore)\b/)) { - return 'RESEARCH'; - } - if (matches(/\b(documentation|docs|readme|wiki|document)\b/)) { - return 'DOCUMENTATION'; - } - if (matches(/\b(planning|project board|plan|roadmap|backlog|estimate|estimation)\b/)) { - return 'PROJECT_MANAGEMENT'; - } - if (matches(/\b(annotation|annotate|labelling|labeling|label)\b/)) { - return 'ANNOTATION'; - } - if (matches(/\b(refactor|fix|fixing|fixed|fixes|bugfix|hotfix|debug|implement|implementation|feature)\b/)) { - return 'DEVELOPMENT'; - } - return undefined; -} - const MY_TIME_ENTRIES_QUERY = gql` query MyTimeEntries($date: Date!) { private { From 8f118c996b921169f949c430c2e0020ba7f9f710 Mon Sep 17 00:00:00 2001 From: tnagorra Date: Sat, 30 May 2026 14:48:13 +0545 Subject: [PATCH 09/11] feat: use sentinel replacemnet before classification --- src/utils/workItemClassifier/index.ts | 317 +++++++++++++++++++------- src/views/DailyJournal/index.tsx | 8 +- 2 files changed, 237 insertions(+), 88 deletions(-) diff --git a/src/utils/workItemClassifier/index.ts b/src/utils/workItemClassifier/index.ts index dbbed9d..e0e263c 100644 --- a/src/utils/workItemClassifier/index.ts +++ b/src/utils/workItemClassifier/index.ts @@ -1,21 +1,23 @@ -// Work-item type classifier — inference side. -// -// Reads a logistic-regression model trained externally (see scripts/README.md -// for the training pipeline and design decisions) and suggests a -// TimeEntryTypeEnum value for a given description. Returns `undefined` when -// the top-class probability is below that class's threshold. -// -// Train/serve contract — this code MUST stay in sync with the analyzer in -// scripts/train_work_item_classifier.py. Specifically: -// - lowercase -// - tokenize on /[^a-z0-9]+/ -// - emit unigrams + adjacent bigrams (bigrams joined with '_') -// - apply IDF + L2 normalize (TF-IDF features) -// The training side additionally substitutes entity names (people, projects, -// clients) with sentinel tokens; this frontend does NOT — entity tokens in -// real descriptions just miss the vocab and are silently ignored, which is -// the desired privacy-preserving behavior. -import type { TimeEntryTypeEnum } from '#generated/types/graphql'; +import { useMemo } from 'react'; +import { + isDefined, + listToGroupList, + listToMap, + mapToList, + mapToMap, + sum, + unique, +} from '@togglecorp/fujs'; +import { + gql, + useQuery, +} from 'urql'; + +import { + TimeEntryTypeEnum, + WorkItemClassifierEntitiesQuery, + WorkItemClassifierEntitiesQueryVariables, +} from '#generated/types/graphql'; import type { WorkItemClassifierModel } from './types'; @@ -23,98 +25,194 @@ import modelData from './model.json'; const model = modelData as unknown as WorkItemClassifierModel; -const vocabIndex = new Map( - model.vocabulary.map((token, idx) => [token, idx]), +const vocabIndex: Record = listToMap( + model.vocabulary, + (token) => token, + (_token, _key, idx) => idx, ); +// We want to replace specific names to either of these +type Sentinel = 'project' | 'client' | 'colleague'; + +// Clean up multi-word phrases +function normalizePhrase(phrase: string): string { + return phrase.toLowerCase().split(/\s+/).filter((p) => p.length > 0).join(' '); +} + +function expandName(name: string, sentinel: Sentinel): [string, Sentinel][] { + const parts = name.split(' '); + const entries: [string, Sentinel][] = [[name, sentinel]]; + if (parts.length >= 2) { + const first = parts[0]; + const last = parts[parts.length - 1]; + if (first !== undefined) { + entries.push([first, sentinel]); + } + if (last !== undefined) { + entries.push([last, sentinel]); + } + } + return entries; +} + +function buildSubstituteMap( + data: WorkItemClassifierEntitiesQuery | undefined, +): Record { + if (!data) { + return {}; + } + const projectNames = unique( + data.private.allProjects.flatMap((p) => [p.name, p.shortName]), + ); + const clientOrgNames = unique([ + ...data.private.clients.items.map((c) => c.name), + ...data.private.contractors.items.map((c) => c.name), + ]); + const userNames = unique( + data.private.users.items.map((u) => u.displayName).filter(isDefined), + ); + + // Priority: project, client, then colleague. + // There are cases where project and client names are the same + const candidates: [string, Sentinel][] = [ + ...projectNames.map<[string, Sentinel]>((p) => [normalizePhrase(p), 'project']), + ...clientOrgNames.map<[string, Sentinel]>((c) => [normalizePhrase(c), 'client']), + ...userNames.flatMap((name) => expandName(normalizePhrase(name), 'colleague')), + ]; + + return candidates.reduce>((acc, [key, sentinel]) => { + if (key.length > 0 && !(key in acc)) { + acc[key] = sentinel; + } + return acc; + }, {}); +} + +function l2Norm(values: number[]): number { + return Math.sqrt(sum(values.map((v) => v * v))); +} + +function softmax(scores: number[]): number[] { + // Subtract the max before exponentiating for numerical stability. + const maxScore = Math.max(...scores); + const exps = scores.map((s) => Math.exp(s - maxScore)); + const denom = sum(exps); + return exps.map((e) => e / denom); +} + +function argmax(values: number[]): number { + return values.indexOf(Math.max(...values)); +} + +function dotProduct( + classWeights: [number, number][], + features: Record, +): number { + return sum(classWeights.map(([tokenIdx, weight]) => { + const cnt = features[tokenIdx]; + return cnt === undefined ? 0 : weight * cnt; + })); +} + +// ─── Tokenization + entity substitution ─────────────────────────────────── + function tokenize(text: string): string[] { return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0); } -function buildFeatures(text: string): Map { - // Mirrors sklearn TfidfTransformer(norm='l2', use_idf=True, smooth_idf=True, - // sublinear_tf=False) applied to count features. Steps: - // 1. Tokenize (unigrams + adjacent bigrams) - // 2. Multiply each count by the token's IDF (from model.idf) - // 3. L2-normalize the row vector - const tokens = tokenize(text); - const rawCounts = new Map(); +// NOTE: Needs space-padded input +function cleanupClientProjectPairs(s: string): string { + // We are recursing until we get no substitution. + // Space-padding both sides to simplify boundary detection without regex + const next = s + .replaceAll(' client project ', ' project ') + .replaceAll(' project client ', ' project ') + .replaceAll(' project project ', ' project '); + return next === s ? s : cleanupClientProjectPairs(next); +} - const bump = (token: string) => { - const idx = vocabIndex.get(token); - if (idx !== undefined) { - rawCounts.set(idx, (rawCounts.get(idx) ?? 0) + 1); - } - }; +function substituteEntities( + tokens: string[], + substituteMap: Record, + sortedPhrases: string[], +): string[] { + // Space-padding both sides to simplify boundary detection without regex + const padded = ` ${tokens.join(' ')} `; + const substituted = sortedPhrases.reduce( + (acc, phrase) => acc.replaceAll(` ${phrase} `, ` ${substituteMap[phrase]} `), + padded, + ); + return cleanupClientProjectPairs(substituted).split(' ').filter((t) => t.length > 0); +} - tokens.forEach((tok) => bump(tok)); - for (let i = 0; i + 1 < tokens.length; i += 1) { - const a = tokens[i]; +function bigrams(tokens: string[]): string[] { + return tokens.slice(0, -1).flatMap((a, i) => { const b = tokens[i + 1]; - if (a !== undefined && b !== undefined) { - bump(`${a}_${b}`); - } - } + return b === undefined ? [] : [`${a}_${b}`]; + }); +} + +function countNgrams(ngrams: string[]): Record { + const vocabHits = ngrams + .map((ngram) => vocabIndex[ngram]) + .filter((idx): idx is number => idx !== undefined); + const grouped = listToGroupList(vocabHits, (idx) => idx); + return mapToMap(grouped, undefined, (group) => group.length); +} + +function buildFeatures( + text: string, + substituteMap: Record, + sortedPhrases: string[], +): Record { + // TfidfTransformer(norm='l2', use_idf=True, smooth_idf=True, sublinear_tf=False) + const tokens = substituteEntities(tokenize(text), substituteMap, sortedPhrases); + const rawCounts = countNgrams([...tokens, ...bigrams(tokens)]); - // tf × idf - const tfidf = new Map(); - rawCounts.forEach((count, idx) => { + const tfidf = mapToMap(rawCounts, undefined, (count, key) => { + const idx = Number(key); const idf = model.idf[idx] ?? 0; - tfidf.set(idx, count * idf); + return count * idf; }); - // L2 normalize - let normSquared = 0; - tfidf.forEach((v) => { normSquared += v * v; }); - const norm = Math.sqrt(normSquared); - if (norm > 0) { - tfidf.forEach((v, idx) => { tfidf.set(idx, v / norm); }); - } + const norm = l2Norm(mapToList(tfidf)); + return norm > 0 + ? mapToMap(tfidf, undefined, (v) => v / norm) + : tfidf; +} - return tfidf; +function classScore( + intercept: number, + classWeights: [number, number][] | undefined, + features: Record, +): number { + if (!classWeights) { + return intercept; + } + return intercept + dotProduct(classWeights, features); } -function inferTypeFromDescription( +function inferType( description: string, + substituteMap: Record, + sortedPhrases: string[], ): TimeEntryTypeEnum | undefined { if (description.trim().length === 0) { return undefined; } - const counts = buildFeatures(description); - if (counts.size === 0) { + const features = buildFeatures(description, substituteMap, sortedPhrases); + if (Object.keys(features).length === 0) { return undefined; } - const scores = model.intercepts.map((intercept, classIdx) => { - let s = intercept; - const classWeights = model.weights[classIdx]; - if (classWeights) { - classWeights.forEach(([tokenIdx, weight]) => { - const cnt = counts.get(tokenIdx); - if (cnt !== undefined) { - s += weight * cnt; - } - }); - } - return s; - }); - - const maxScore = Math.max(...scores); - const exps = scores.map((s) => Math.exp(s - maxScore)); - const sum = exps.reduce((a, b) => a + b, 0); - - let bestIdx = 0; - let bestExp = exps[0] ?? 0; - for (let c = 1; c < exps.length; c += 1) { - const e = exps[c]; - if (e !== undefined && e > bestExp) { - bestExp = e; - bestIdx = c; - } - } + const scores = model.intercepts.map( + (intercept, classIdx) => classScore(intercept, model.weights[classIdx], features), + ); + const probabilities = softmax(scores); + const bestIdx = argmax(probabilities); - const bestProb = bestExp / sum; + const bestProb = probabilities[bestIdx] ?? 0; const classThreshold = model.thresholds[bestIdx] ?? 1.0; if (bestProb < classThreshold) { return undefined; @@ -123,4 +221,53 @@ function inferTypeFromDescription( return model.classes[bestIdx]; } -export default inferTypeFromDescription; +const WORK_ITEM_CLASSIFIER_ENTITIES_QUERY = gql` + query WorkItemClassifierEntities { + private { + id + allProjects { + id + name + shortName + } + clients(pagination: { limit: 9999 }) { + items { + id + name + } + } + contractors(pagination: { limit: 9999 }) { + items { + id + name + } + } + users(pagination: { limit: 9999 }) { + items { + id + displayName + } + } + } + } +`; + +function useWorkItemClassifier(): (description: string) => TimeEntryTypeEnum | undefined { + const [result] = useQuery< + WorkItemClassifierEntitiesQuery, + WorkItemClassifierEntitiesQueryVariables + >({ + query: WORK_ITEM_CLASSIFIER_ENTITIES_QUERY, + requestPolicy: 'cache-and-network', + }); + const { data } = result; + + return useMemo(() => { + const substituteMap = buildSubstituteMap(data); + // Sort phrases longest-first so multi-word names win over their parts + const sortedPhrases = Object.keys(substituteMap).sort((a, b) => b.length - a.length); + return (description: string) => inferType(description, substituteMap, sortedPhrases); + }, [data]); +} + +export default useWorkItemClassifier; diff --git a/src/views/DailyJournal/index.tsx b/src/views/DailyJournal/index.tsx index 0026a86..4aa7143 100644 --- a/src/views/DailyJournal/index.tsx +++ b/src/views/DailyJournal/index.tsx @@ -62,7 +62,7 @@ import { Task, WorkItem, } from '#utils/types'; -import inferTypeFromDescription from '#utils/workItemClassifier'; +import useWorkItemClassifier from '#utils/workItemClassifier'; import AddWorkItemDialog from './AddWorkItemDialog'; import AvailabilityDialog from './AvailabilityDialog'; @@ -124,6 +124,8 @@ export function Component() { const { midActionsRef } = useContext(NavbarContext); + const inferTypeFromDescription = useWorkItemClassifier(); + const { date: dateFromParams } = useParams<{ date: string | undefined}>(); const { fullDate } = useContext(DateContext); const selectedDate = useMemo(() => { @@ -335,7 +337,7 @@ export function Component() { dialogOpenTriggerRef.current(override.description ?? undefined); } }, - [], + [inferTypeFromDescription], ); const handleWorkItemClone = useCallback( @@ -438,7 +440,7 @@ export function Component() { ); } }, - [workItems, setWorkItemChange, selectedDate], + [workItems, setWorkItemChange, selectedDate, inferTypeFromDescription], ); const handleWorkItemDelete = useCallback( From 6c18b7f23ee3a55b48871e7b5f0457e36d378586 Mon Sep 17 00:00:00 2001 From: tnagorra Date: Sat, 30 May 2026 14:50:25 +0545 Subject: [PATCH 10/11] feat: add feature to auto-infer type on focus out - add setting to disable this feature --- src/utils/constants.ts | 1 + src/utils/types.ts | 1 + .../DayView/WorkItemRow/index.tsx | 41 +++++++++++++++++-- src/views/DailyJournal/DayView/index.tsx | 3 ++ src/views/DailyJournal/index.tsx | 1 + src/views/Settings/index.tsx | 7 ++++ 6 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/utils/constants.ts b/src/utils/constants.ts index aed816b..646a37d 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -17,6 +17,7 @@ export const defaultConfigValue: ConfigStorage = { checkboxForStatus: false, enableCollapsibleGroups: false, enableStrikethrough: false, + autoInferTypeOnBlur: true, startSidebarShown: window.innerWidth >= 900, endSidebarShown: false, dailyJournalGrouping: { diff --git a/src/utils/types.ts b/src/utils/types.ts index 58b7237..5831e35 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -56,6 +56,7 @@ export type ConfigStorage = { indent: boolean, enableCollapsibleGroups: boolean, enableStrikethrough: boolean, + autoInferTypeOnBlur: boolean, dailyJournalAttributeOrder: DailyJournalAttribute[]; dailyJournalGrouping: DailyJournalGrouping; diff --git a/src/views/DailyJournal/DayView/WorkItemRow/index.tsx b/src/views/DailyJournal/DayView/WorkItemRow/index.tsx index c714d71..060f2df 100644 --- a/src/views/DailyJournal/DayView/WorkItemRow/index.tsx +++ b/src/views/DailyJournal/DayView/WorkItemRow/index.tsx @@ -99,6 +99,7 @@ interface Props { onAssist?: (clientId: string) => void; onChange?: (clientId: string, ...entries: EntriesAsList) => void; onDelete?: (clientId: string) => void; + inferTypeFromDescription?: (description: string) => WorkItem['type']; } function WorkItemRow(props: Props) { @@ -112,6 +113,7 @@ function WorkItemRow(props: Props) { onChange, typeErrored, durationErrored, + inferTypeFromDescription, } = props; const { enums } = useContext(EnumsContext); @@ -129,6 +131,29 @@ function WorkItemRow(props: Props) { [workItem.clientId, onChange], ); + const handleDescriptionBlur = useCallback( + () => { + if (!config.autoInferTypeOnBlur || workItem.type || !inferTypeFromDescription) { + return; + } + const description = workItem.description?.trim(); + if (!description) { + return; + } + const inferred = inferTypeFromDescription(description); + if (inferred) { + setFieldValue(inferred, 'type'); + } + }, + [ + config.autoInferTypeOnBlur, + workItem.type, + workItem.description, + inferTypeFromDescription, + setFieldValue, + ], + ); + const taskList: Task[] = useMemo( () => ( unique( @@ -215,20 +240,27 @@ function WorkItemRow(props: Props) { const handleClone = useCallback( () => { if (onClone) { - onClone(workItem.clientId, { duration: undefined, description: undefined }); + onClone(workItem.clientId, { + duration: undefined, + description: undefined, + ...(config.autoInferTypeOnBlur && { type: undefined }), + }); } }, - [onClone, workItem.clientId], + [onClone, workItem.clientId, config.autoInferTypeOnBlur], ); const handleCloneWithDescription = useCallback( () => { if (onClone) { // NOTE: we only want to clear duration - onClone(workItem.clientId, { duration: undefined }); + onClone(workItem.clientId, { + duration: undefined, + ...(config.autoInferTypeOnBlur && { type: undefined }), + }); } }, - [onClone, workItem.clientId], + [onClone, workItem.clientId, config.autoInferTypeOnBlur], ); const handleShortcuts = useCallback( @@ -301,6 +333,7 @@ function WorkItemRow(props: Props) { value={workItem.description} onChange={setFieldValue} onKeyDown={handleShortcuts} + onBlur={handleDescriptionBlur} placeholder="Description" compact={config.compactTextArea} /> diff --git a/src/views/DailyJournal/DayView/index.tsx b/src/views/DailyJournal/DayView/index.tsx index d8b6454..4c3f4d6 100644 --- a/src/views/DailyJournal/DayView/index.tsx +++ b/src/views/DailyJournal/DayView/index.tsx @@ -73,6 +73,7 @@ interface Props { onWorkItemChange: (clientId: string, ...entries: EntriesAsList) => void; onWorkItemDelete: (clientId: string) => void; selectedDate: string; + inferTypeFromDescription?: (description: string) => WorkItem['type']; } function DayView(props: Props) { @@ -87,6 +88,7 @@ function DayView(props: Props) { errored, selectedDate, tasks, + inferTypeFromDescription, } = props; const { taskById: oldTaskById } = useContext(EnumsContext); @@ -471,6 +473,7 @@ function DayView(props: Props) { onAssist={onWorkItemAssist} onChange={onWorkItemChange} onDelete={onWorkItemDelete} + inferTypeFromDescription={inferTypeFromDescription} />
); diff --git a/src/views/DailyJournal/index.tsx b/src/views/DailyJournal/index.tsx index 4aa7143..c5c2b73 100644 --- a/src/views/DailyJournal/index.tsx +++ b/src/views/DailyJournal/index.tsx @@ -747,6 +747,7 @@ export function Component() { onWorkItemChange={handleWorkItemChange} onWorkItemDelete={handleWorkItemDelete} selectedDate={selectedDate} + inferTypeFromDescription={inferTypeFromDescription} />