Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
68ff925
Changed all hotkeys combinations to alt+ctrl and added info in toolti…
Bendixx Aug 21, 2025
b4c4e55
Shortcuts for translate, bulletlist, empathic and copy is now only tr…
Bendixx Aug 21, 2025
6fd4d86
accepting new conversation in autofocus, when using hotkeys (better f…
Bendixx Aug 21, 2025
05a4575
removed alt+ctrl for all hotkeys and changed tooltip description + ad…
Bendixx Aug 22, 2025
fcd650d
disabled function for removing whitespace when paste text in inputfield
Bendixx Aug 25, 2025
dc7da5b
Merge pull request #165 from navikt/paste-text-bug
Bendixx Aug 25, 2025
86e5888
added warning when pasting text
Bendixx Aug 26, 2025
65a967e
Merge pull request #166 from navikt/paste-text-bug
Bendixx Aug 26, 2025
c92f349
Removing 2-5 new lines with white space everytime you copy and paste …
Bendixx Aug 26, 2025
7694f32
Merge pull request #167 from navikt/paste-text-bug
Bendixx Aug 26, 2025
ec92132
Changed all hotkeys combinations to alt+ctrl and added info in toolti…
Bendixx Aug 21, 2025
efdfd0a
Shortcuts for translate, bulletlist, empathic and copy is now only tr…
Bendixx Aug 21, 2025
cd4742c
accepting new conversation in autofocus, when using hotkeys (better f…
Bendixx Aug 21, 2025
965bd71
removed alt+ctrl for all hotkeys and changed tooltip description + ad…
Bendixx Aug 22, 2025
9beac35
removed tabindex for send button and navlogo (for user testing 27-08-25)
Bendixx Aug 26, 2025
e157bb7
Merge remote-tracking branch 'origin/keyboard-shortcuts' into keyboar…
Bendixx Aug 26, 2025
2b2226a
Changes for user testing (alt+ctrl combos)
Bendixx Aug 27, 2025
247676d
hotkeys now working with alt+ctrl combos when textarea is active, and…
Bendixx Aug 28, 2025
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
28 changes: 28 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"rehype-raw": "^7.0.0",
"rehype-stringify": "^10.0.1",
"remark": "^15.0.1",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"remark-rehype": "^11.1.2",
"remark-stringify": "^11.0.0",
Expand Down
26 changes: 16 additions & 10 deletions src/components/content/ConversationContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { useParams, useSearchParams } from "react-router"
import { useSendMessage } from "../../api/sse.ts"

import { ArrowDownIcon } from "@navikt/aksel-icons"
import { Alert as AlertComponent, Button, Heading } from "@navikt/ds-react"
import { Alert as AlertComponent, Button, Heading, Tooltip } from "@navikt/ds-react"
import { useEffect, useLayoutEffect, useRef, useState } from "react"
import { useHotkeys } from "react-hotkeys-hook"
import Markdown from "react-markdown"
import { useAlerts, useMessages } from "../../api/api.ts"
import { NewMessage } from "../../types/Message.ts"
Expand Down Expand Up @@ -107,7 +108,9 @@ function ConversationContent() {
})
}

// useHotkeys("ctrl+b", () => scrollToBottom())
useHotkeys("alt+ctrl+B", () => scrollToBottom(), {
enableOnFormTags: true,
})

return (
<div className='conversation-content'>
Expand All @@ -129,14 +132,17 @@ function ConversationContent() {
)}
</div>
{showScrollButton && (
<Button
icon={<ArrowDownIcon />}
className='fixed left-1/2 -translate-x-1/2'
style={{ bottom: dynamicBottom }}
variant='primary-neutral'
size='small'
onClick={scrollToBottom}
/>
<Tooltip content='Scroll til bunnen ( Alt+Ctrl+B )'>
<Button
icon={<ArrowDownIcon />}
className='fixed left-1/2 -translate-x-1/2'
style={{ bottom: dynamicBottom }}
variant='primary-neutral'
size='small'
onClick={scrollToBottom}
aria-label='Scroll til bunnen'
/>
</Tooltip>
)}
<InputField
onSend={handleUserMessage}
Expand Down
14 changes: 12 additions & 2 deletions src/components/content/chat/ChatContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Fragment, useEffect, useRef, useState } from "react"
import React, { Fragment, useEffect, useRef, useState } from "react"
import { useSearchParams } from "react-router"
import { Message, NewMessage } from "../../../types/Message.ts"
import { BobAnswerBubble } from "./chatbubbles/answerbubble/BobAnswerBubble.tsx"
Expand Down Expand Up @@ -45,8 +45,18 @@ function ChatContainer({ messages, onSend, isLoading }: ChatDialogProps) {
prevMessagesLength.current = messages.length
}, [messages, lastMessageRef, selectedMessageRef])

const handleCopy: React.ClipboardEventHandler<HTMLDivElement> = (e) => {
const sel = window.getSelection()?.toString()
if (!sel) return
e.preventDefault()
e.clipboardData.setData("text/plain", sel.trimEnd())
}

return (
<div className='dialogcontent h-auto grow flex-col px-4 pt-4'>
<div
className='dialogcontent h-auto grow flex-col px-4 pt-4'
onCopy={handleCopy}
>
{messages.map((message, index) =>
message.messageRole === "human" ? (
<Fragment key={message.id}>
Expand Down
65 changes: 32 additions & 33 deletions src/components/content/chat/chatbubbles/BobAnswerCitations.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import { ExternalLinkIcon } from "@navikt/aksel-icons"
import {
BodyLong,
BodyShort,
Detail,
Label,
Link,
Tooltip,
} from "@navikt/ds-react"
import { BodyLong, BodyShort, Detail, Label, Link, Tooltip } from "@navikt/ds-react"
import Markdown from "react-markdown"
import remarkGfm from "remark-gfm"
import { KunnskapsbasenIcon } from "../../../../assets/icons/KunnskapsbasenIcon.tsx"
Expand Down Expand Up @@ -46,16 +39,13 @@ function BobAnswerCitations({ citation, context }: BobAnswerCitationProps) {

export default BobAnswerCitations

const SingleCitation = ({
citation,
context,
}: {
citation: Citation
context: Context | undefined
}) => {
const SingleCitation = ({ citation, context }: { citation: Citation; context: Context | undefined }) => {
return (
<div className='mb-2 flex flex-col'>
<Label size='small' className='mb-1'>
<Label
size='small'
className='mb-1'
>
{context ? (
<div className='flex flex-wrap gap-2'>
<CitationLink
Expand All @@ -65,18 +55,23 @@ const SingleCitation = ({
<SourceIcon source={context.source} />
</div>
) : (
<BodyShort size='medium'>
Kunne ikke finne lenke til artikkelen.
</BodyShort>
<BodyShort size='medium'>Kunne ikke finne lenke til artikkelen.</BodyShort>
)}
</Label>
<BodyLong size='small' className='mt-1 italic'>
<BodyLong
size='small'
className='mt-1 italic'
>
<Markdown
className='markdown'
remarkPlugins={[remarkGfm]}
components={{
a: ({ ...props }) => (
<a {...props} target='_blank' rel='noopener noreferrer' />
<a
{...props}
target='_blank'
rel='noopener noreferrer'
/>
),
}}
>
Expand All @@ -101,10 +96,16 @@ const MultiCitation = ({
const articleLink = contexts.at(citations[0]!.sourceId)!.url
return (
<div className='mb-2 flex flex-col'>
<Label size='small' className='mb-1'>
<Label
size='small'
className='mb-1'
>
<div className='flex flex-wrap gap-2'>
<Tooltip content='Åpner artikkelen i ny fane'>
<Link href={articleLink} target='_blank'>
<Link
href={articleLink}
target='_blank'
>
{title}
<ExternalLinkIcon />
</Link>
Expand All @@ -121,7 +122,11 @@ const MultiCitation = ({
remarkPlugins={[remarkGfm]}
components={{
a: ({ ...props }) => (
<a {...props} target='_blank' rel='noopener noreferrer' />
<a
{...props}
target='_blank'
rel='noopener noreferrer'
/>
),
}}
>
Expand Down Expand Up @@ -165,19 +170,13 @@ const CitationLink = ({

// Encoding for RFC3986 - making text fragments to work for citations with unreserved marks //
function encodeFragment(text: string) {
return encodeURIComponent(text).replace(
/[-!'()*#]/g,
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,
)
return encodeURIComponent(text).replace(/[-!'()*#]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)
}

// Expands all panels on the nav.no-page //
const expandAll =
matchingContextCitationData?.source === "navno" ? "?expandall=true" : ""
const expandAll = matchingContextCitationData?.source === "navno" ? "?expandall=true" : ""

const useAnchor = matchingContextCitationData?.url.includes(
"/saksbehandlingstider",
)
const useAnchor = matchingContextCitationData?.url.includes("/saksbehandlingstider")

return (
<Tooltip content='Åpner artikkelen i ny fane'>
Expand Down
18 changes: 12 additions & 6 deletions src/components/content/chat/chatbubbles/UserQuestionBubble.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Message } from "../../../../types/Message.ts"

import { PencilWritingIcon } from "@navikt/aksel-icons"
import { BodyLong, Button, Heading, Tooltip } from "@navikt/ds-react"
import { Button, Heading, Tooltip } from "@navikt/ds-react"
import { memo } from "react"
import Markdown from "react-markdown"
import rehypeRaw from "rehype-raw"
import remarkBreaks from "remark-breaks"
import remarkGfm from "remark-gfm"
import analytics from "../../../../utils/analytics.ts"
import { useInputFieldStore } from "../../../inputfield/InputField.tsx"
import "./ChatBubbles.css"
Expand All @@ -15,7 +17,8 @@ interface UserChatBubbleProps {

const UserQuestionBubble = memo(
({ userQuestion }: UserChatBubbleProps) => {
const question = userQuestion?.content.replace(/\n/g, "<br>")
const raw = userQuestion?.content?.trimEnd() ?? ""
// const question = userQuestion?.content.replace(/\n/g, "<br>")

const { setInputValue, focusTextarea } = useInputFieldStore()

Expand All @@ -28,7 +31,7 @@ const UserQuestionBubble = memo(
}

return (
<div className='questionhover mb-2 flex w-fit flex-row items-end gap-1 self-end'>
<div className='questionhover mb-4 flex w-fit flex-row items-end gap-1 self-end'>
<div className='hide-show-edit fade-in hidden'>
<Tooltip
content='Rediger spørsmålet'
Expand All @@ -51,9 +54,12 @@ const UserQuestionBubble = memo(
>
Du spurte:
</Heading>
<BodyLong>
<Markdown rehypePlugins={[rehypeRaw]}>{question}</Markdown>
</BodyLong>
<Markdown
remarkPlugins={[remarkGfm, remarkBreaks]}
rehypePlugins={[rehypeRaw]}
>
{raw}
</Markdown>
</div>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,49 +164,47 @@ const MessageContent = ({
}

return (
<VStack gap='5'>
<div className='flex flex-col gap-5'>
<Heading
size='small'
className='sr-only top-0'
level='2'
>
Svar fra Bob:
</Heading>
<BodyLong className='fade-in'>
<Markdown
className='markdown'
remarkPlugins={[md.remarkCitations]}
rehypePlugins={[rehypeRaw]}
components={{
a: ({ ...props }) => (
<a
{...props}
target='_blank'
rel='noopener noreferrer'
/>
),
span: (props: CitationSpanProps) => {
const dataCitation = props["data-citation"]
const dataPosition = props["data-position"]
if (dataCitation && dataPosition) {
const citationId = parseInt(dataCitation, 10)
addCitation(citationId, parseInt(dataPosition, 10))
return (
<CitationNumber
citations={citations}
citationId={citationId}
context={message.context}
/>
)
}
return <span {...props} />
},
}}
>
{message.content}
</Markdown>
</BodyLong>
</VStack>
<Markdown
className='markdown'
remarkPlugins={[md.remarkCitations]}
rehypePlugins={[rehypeRaw]}
components={{
a: ({ ...props }) => (
<a
{...props}
target='_blank'
rel='noopener noreferrer'
/>
),
span: (props: CitationSpanProps) => {
const dataCitation = props["data-citation"]
const dataPosition = props["data-position"]
if (dataCitation && dataPosition) {
const citationId = parseInt(dataCitation, 10)
addCitation(citationId, parseInt(dataPosition, 10))
return (
<CitationNumber
citations={citations}
citationId={citationId}
context={message.context}
/>
)
}
return <span {...props} />
},
}}
>
{message.content}
</Markdown>
</div>
)
}

Expand Down
Loading