Skip to content

Commit

Permalink
keep claim extraction state in local storage (#204)
Browse files Browse the repository at this point in the history
  • Loading branch information
JohannesNakayama authored Sep 11, 2024
1 parent 67722df commit 9cdd6a0
Showing 1 changed file with 163 additions and 82 deletions.
245 changes: 163 additions & 82 deletions app/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Link, useLoaderData } from '@remix-run/react'
import moment from 'moment'
import { useState } from 'react'
import { type ChangeEvent, useState } from 'react'
import { Markdown } from '#app/components/markdown.tsx'
import { Icon } from '#app/components/ui/icon.tsx'
import PollResult from '#app/components/ui/poll-result.tsx'
import { PostContent } from '#app/components/ui/post-content.tsx'
import { Textarea } from '#app/components/ui/textarea.tsx'
Expand All @@ -10,6 +11,7 @@ import { db } from '#app/db.ts'
import { type ClaimList } from '#app/repositories/fact-checking.ts'
import { getChronologicalPolls } from '#app/repositories/ranking.ts'
import { PollType, type FrontPagePost } from '#app/types/api-types.ts'
import { useDebounce } from '#app/utils/misc.tsx'
import { useOptionalUser } from '#app/utils/user.ts'

export async function loader() {
Expand All @@ -22,33 +24,6 @@ export async function loader() {
export default function ClaimExtraction() {
const { feed } = useLoaderData<typeof loader>()

const [statementValue, setStatementValue] = useState<string>('')
const [originValue, setOriginValue] = useState<string>('')
const [isExtractingClaims, setIsExtractingClaims] = useState(false)
const [claims, setClaims] = useState<ClaimList>({
claim_context: '',
extracted_claims: [],
})
const [urlError, setUrlError] = useState<boolean>(false)

async function handleExtractClaims() {
setIsExtractingClaims(true)
try {
const payload = {
content: statementValue,
}
const response = await fetch('/extractClaims', {
method: 'POST',
body: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' },
})
const newExtractedClaims = (await response.json()) as ClaimList
setClaims(newExtractedClaims)
} finally {
setIsExtractingClaims(false)
}
}

const infoText = `
## Jabble Polls
Expand All @@ -60,72 +35,48 @@ You can then decide which ones you want to post.
You can also add an origin URL to give context to where you found the statement.
`

const disclaimer = `
Press **Ctrl + Enter** to extract claims.
**Disclaimer**: Your text will be sent to the OpenAI API for analysis.
`
const [showClaimExtractionForm, setShowClaimExtractionForm] =
useState<boolean>(false)

return (
<div>
<div className="mb-4 flex flex-col space-y-2 rounded-xl border-2 border-solid border-gray-200 p-4 text-sm dark:border-gray-700">
<div className="mb-4">
<Markdown deactivateLinks={false}>{infoText}</Markdown>
</div>
<Textarea
placeholder="A statement to extract claims from."
name="content"
value={statementValue}
maxLength={MAX_CHARS_PER_DOCUMENT}
onChange={event => setStatementValue(event.target.value)}
className="mb-2 min-h-[150px] w-full"
onKeyDown={(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.ctrlKey && event.key === 'Enter') {
event.preventDefault() // Prevent default behavior if needed
handleExtractClaims()
}
}}
/>
<Textarea
placeholder="URL (optional, where the statement was made)"
name="origin-url"
value={originValue}
onChange={event => {
setOriginValue(event.target.value)
isValidUrl(event.target.value)
? setUrlError(false)
: setUrlError(true)
}}
className={
'mb-2 h-4 w-full ' +
(urlError && originValue !== '' ? 'border-2 border-red-500' : '')
}
onKeyDown={(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.ctrlKey && event.key === 'Enter') {
event.preventDefault() // Prevent default behavior if needed
handleExtractClaims()
}
}}
/>
{urlError && originValue !== '' && (
<div className="text-sm text-red-500">Please enter a valid URL.</div>
)}
<div className="mb-6 flex flex-row">
<div className="mr-auto self-end text-gray-500">
<Markdown deactivateLinks={false}>{disclaimer}</Markdown>
</div>
<div className="text-md flex w-full">
<button
title="Ctrl + Enter"
disabled={isExtractingClaims}
className="rounded bg-purple-200 px-4 py-2 text-base font-bold text-black hover:bg-purple-300"
onClick={e => {
e.preventDefault()
handleExtractClaims()
onClick={() => {
setShowClaimExtractionForm(!showClaimExtractionForm)
return false
}}
className="shrink-0 font-bold text-purple-700 dark:text-purple-200"
>
{isExtractingClaims ? 'Extracting Claims...' : 'Extract Claims'}
{showClaimExtractionForm ? (
<Icon name="chevron-down">Start extracting claims</Icon>
) : (
<Icon name="chevron-right">Start extracting claims</Icon>
)}
</button>
{showClaimExtractionForm && (
<button
className="ml-auto self-center pr-2"
onClick={() => setShowClaimExtractionForm(false)}
>
</button>
)}
</div>
<ExtractedClaimList claims={claims} origin={originValue} />
{
/*
This is a hack. The localStorage object is only accessible on the
client-side, so we have to make sure this component is not rendered on
the server. There are other ways to do this (which are also hacky), but
for the time being, it's easiest to just hide this form and render it on
click on a button.
*/
showClaimExtractionForm && <ClaimExtractionForm />
}
</div>
<div>
<div className="mb-5 px-4">
Expand All @@ -139,6 +90,136 @@ Press **Ctrl + Enter** to extract claims.
)
}

function ClaimExtractionForm() {
const statementValueStorageKey = 'claim-extraction-statement'
const [statementValue, setStatementValue] = useState<string>(
() => localStorage.getItem(statementValueStorageKey) ?? '',
)
const statementValueChangeHandler = useDebounce(
(event: ChangeEvent<HTMLTextAreaElement>) => {
localStorage.removeItem(claimsStorageKey)
localStorage.setItem(statementValueStorageKey, event.target.value)
},
500,
)

const originValueStorageKey = 'claim-extraction-origin'
const [originValue, setOriginValue] = useState<string>(
() => localStorage.getItem(originValueStorageKey) ?? '',
)
const originValueChangeHandler = useDebounce(
(event: ChangeEvent<HTMLTextAreaElement>) => {
localStorage.setItem(originValueStorageKey, event.target.value)
},
500,
)

const claimsStorageKey = 'extracted-claims'
const [claims, setClaims] = useState<ClaimList>(() => {
const claimsFromLocalStorage = localStorage.getItem(claimsStorageKey)
if (claimsFromLocalStorage == null) {
return {
claim_context: '',
extracted_claims: [],
}
}
return JSON.parse(claimsFromLocalStorage) as ClaimList
})

const [isExtractingClaims, setIsExtractingClaims] = useState(false)
const [urlError, setUrlError] = useState<boolean>(
() => (!isValidUrl(originValue) && !(originValue == '')) || false,
)

async function handleExtractClaims() {
setIsExtractingClaims(true)
try {
const payload = {
content: statementValue,
}
const response = await fetch('/extractClaims', {
method: 'POST',
body: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' },
})
const newExtractedClaims = (await response.json()) as ClaimList
setClaims(newExtractedClaims)
localStorage.setItem(claimsStorageKey, JSON.stringify(newExtractedClaims))
} finally {
setIsExtractingClaims(false)
}
}

const disclaimer = `
Press **Ctrl + Enter** to extract claims.
**Disclaimer**: Your text will be sent to the OpenAI API for analysis.
`

return (
<>
<Textarea
placeholder="A statement to extract claims from."
name="content"
value={statementValue}
maxLength={MAX_CHARS_PER_DOCUMENT}
onChange={event => {
statementValueChangeHandler(event)
setStatementValue(event.target.value)
}}
className="mb-2 min-h-[150px] w-full"
onKeyDown={(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.ctrlKey && event.key === 'Enter') {
event.preventDefault() // Prevent default behavior if needed
handleExtractClaims()
}
}}
/>
<Textarea
placeholder="URL (optional, where the statement was made)"
name="origin-url"
value={originValue}
onChange={event => {
originValueChangeHandler(event)
setOriginValue(event.target.value)
isValidUrl(event.target.value)
? setUrlError(false)
: setUrlError(true)
}}
className={
'mb-2 h-4 w-full ' +
(urlError && originValue !== '' ? 'border-2 border-red-500' : '')
}
onKeyDown={(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.ctrlKey && event.key === 'Enter') {
event.preventDefault() // Prevent default behavior if needed
handleExtractClaims()
}
}}
/>
{urlError && originValue !== '' && (
<div className="text-sm text-red-500">Please enter a valid URL.</div>
)}
<div className="mb-6 flex flex-row">
<div className="mr-auto self-end text-gray-500">
<Markdown deactivateLinks={false}>{disclaimer}</Markdown>
</div>
<button
title="Ctrl + Enter"
disabled={isExtractingClaims}
className="rounded bg-purple-200 px-4 py-2 text-base font-bold text-black hover:bg-purple-300"
onClick={e => {
e.preventDefault()
handleExtractClaims()
}}
>
{isExtractingClaims ? 'Extracting Claims...' : 'Extract Claims'}
</button>
</div>
<ExtractedClaimList claims={claims} origin={originValue} />
</>
)
}

type Claim = {
claim: string
claim_without_indirection: string
Expand Down

0 comments on commit 9cdd6a0

Please sign in to comment.