Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

keep claim extraction state in local storage #204

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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