Skip to content
Merged
Show file tree
Hide file tree
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
24 changes: 15 additions & 9 deletions modules/openai/chat-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ function personalos_map_notebook_to_para_item( $notebook ) {
);
}

/**
* Get messages from a post and parse them into UIMessage format
*
* @param int $post_id The post ID to retrieve messages from.
* @return array Parsed messages.
*/
/**
* Get messages from a post and parse them into UIMessage format
*
Expand All @@ -38,18 +44,18 @@ function personalos_get_messages_from_post( $post_id ) {
$messages = array();

foreach ( $blocks as $block ) {
if ( $block['blockName'] === 'pos/ai-message' ) {
if ( 'pos/ai-message' === $block['blockName'] ) {
$role = $block['attrs']['role'] ?? 'user';
// Handle different content structures if necessary, for now assume string content in attrs or innerContent
// Gutenberg blocks usually store content in innerContent for HTML, but pos/ai-message might be different.
// Looking at save_backscroll implementation:
// $content_blocks[] = get_comment_delimited_block_content( ... 'content' => $content ... )
// This usually implies attribute storage or innerHTML if it's a dynamic block saving.
// However, save_backscroll uses get_comment_delimited_block_content which suggests attributes serialization.

$content = $block['attrs']['content'] ?? '';
$id = $block['attrs']['id'] ?? 'generated_' . uniqid();

// Content is stored in innerHTML as <span class="ai-message-text">...</span>
$content = '';
if ( ! empty( $block['innerHTML'] ) ) {
if ( preg_match( '/<span class="ai-message-text">(.*?)<\/span>/s', $block['innerHTML'], $matches ) ) {
$content = html_entity_decode( $matches[1], ENT_QUOTES, 'UTF-8' );
}
}

$messages[] = array(
'id' => $id,
'role' => $role,
Expand Down
15 changes: 10 additions & 5 deletions modules/openai/class-openai-module.php
Original file line number Diff line number Diff line change
Expand Up @@ -1944,15 +1944,16 @@ public function save_backscroll( array $backscroll, array $search_args, bool $ap
}
}

// Create message block
// Create message block with content in innerHTML (not attributes)
// This preserves newlines naturally without escaping hacks
$inner_html = '<span class="ai-message-text">' . esc_html( $content ) . '</span>';
$content_blocks[] = get_comment_delimited_block_content(
'pos/ai-message',
array(
'role' => $role,
'content' => $content,
'id' => $message_id,
'role' => $role,
'id' => $message_id,
),
''
$inner_html
);
}
}
Expand Down Expand Up @@ -1992,6 +1993,9 @@ public function save_backscroll( array $backscroll, array $search_args, bool $ap
$post_data['post_content'] = implode( "\n\n", $content_blocks );
}

// Note: wp_slash is handled by preserve_ai_message_newlines filter
// to protect escaped newlines from wp_unslash

// Only update if there is content to update or we are not just appending empty content
// But here we might want to update just to ensure touch?
if ( isset( $post_data['post_content'] ) || isset( $post_data['post_title'] ) || ! $append ) {
Expand All @@ -2001,6 +2005,7 @@ public function save_backscroll( array $backscroll, array $search_args, bool $ap
}
} else {
// Create new
// Note: wp_slash is handled by preserve_ai_message_newlines filter
$post_data['post_content'] = implode( "\n\n", $content_blocks );
// Ensure defaults for new post
if ( empty( $post_data['post_title'] ) ) {
Expand Down
24 changes: 24 additions & 0 deletions src-chatbot/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,32 @@
@apply border-border;
}

html, body {
overflow-x: hidden;
width: 100%;
max-width: 100vw;
}

body {
@apply bg-background text-foreground;
/* PWA safe area handling for iOS notch and home indicator */
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
min-height: 100vh;
min-height: 100dvh;
}

/* Ensure proper text wrapping everywhere */
p, li, a, span, div {
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
}

a {
word-break: break-all;
}
}

Expand Down
8 changes: 5 additions & 3 deletions src-chatbot/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import ClientOnly from '@/components/client-only';
import { PWARegister } from '@/components/pwa-register';
// import { GeistSans } from 'geist/font/sans'; // Removed due to resolution issues

import './globals.css';
Expand All @@ -13,7 +14,7 @@ export const metadata: Metadata = {
applicationName: 'PersonalOS',
appleWebApp: {
capable: true,
statusBarStyle: 'default',
statusBarStyle: 'black-translucent',
title: 'Personal Chat',
// startupImage: [
// '/images/apple-touch-startup-image-768x1004.png',
Expand All @@ -32,7 +33,7 @@ export const metadata: Metadata = {
{ url: '/wp-content/plugins/personalos/build/chatbot/images/apple-icon.png', sizes: '180x180', type: 'image/png' },
],
},
// manifest: '/wp-content/plugins/personalos/build/chatbot/manifest.json',
manifest: '/wp-content/plugins/personalos/build/chatbot/manifest.json',
};

export const viewport = {
Expand Down Expand Up @@ -90,7 +91,7 @@ export default async function RootLayout({
>
<head>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Personal Chat" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
Expand All @@ -102,6 +103,7 @@ export default async function RootLayout({
/>
</head>
<body className="antialiased">
<PWARegister />
<ClientOnly>{children}</ClientOnly>
</body>
</html>
Expand Down
26 changes: 6 additions & 20 deletions src-chatbot/components/markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,11 @@ import remarkGfm from 'remark-gfm';
import { CodeBlock } from './code-block';

/**
* Decode HTML entities and normalize text for markdown rendering
* Handles cases where newlines are encoded as "n" or "nn" characters
* Decode HTML entities for markdown rendering.
*/
function decodeAndNormalizeText(text: string): string {
function decodeHtmlEntities(text: string): string {
let decoded = text;

// Remove HTML tags but preserve their content
decoded = decoded.replace(/<[^>]*>/g, '');

// Replace encoded newlines: "nn" = double newline, "n" = single newline
// Pattern: "nn" followed by "-" or space, or "n" followed by "-" or space
// Replace "nn" first (double newline) before single "n" to avoid double replacement
decoded = decoded.replace(/nn(?=\s*-)/g, '\n\n');
decoded = decoded.replace(/n(?=\s*-)/g, '\n');

// Also handle cases where "nn" or "n" appears at end of line or before other whitespace
decoded = decoded.replace(/nn(?=\s)/g, '\n\n');
decoded = decoded.replace(/(?<!\n)n(?=\s)/g, '\n');

// Decode common HTML entities
decoded = decoded
.replace(/&nbsp;/g, ' ')
Expand All @@ -41,7 +27,7 @@ function decodeAndNormalizeText(text: string): string {
decoded = textarea.value;
}

return decoded.trim();
return decoded;
}

const components: Partial<Components> = {
Expand Down Expand Up @@ -136,12 +122,12 @@ const components: Partial<Components> = {
const remarkPlugins = [remarkGfm];

const NonMemoizedMarkdown = ({ children }: { children: string }) => {
// Decode HTML entities and normalize the text before rendering
const normalizedText = decodeAndNormalizeText(children);
// Decode HTML entities before rendering
const decodedText = decodeHtmlEntities(children);

return (
<ReactMarkdown remarkPlugins={remarkPlugins} components={components}>
{normalizedText}
{decodedText}
</ReactMarkdown>
);
};
Expand Down
19 changes: 19 additions & 0 deletions src-chatbot/components/pwa-register.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client';

import { useEffect } from 'react';

export function PWARegister() {
useEffect(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/wp-content/plugins/personalos/build/chatbot/sw.js')
.catch((error) => {
console.log('Service worker registration failed:', error);
});
}
}, []);

return null;
}


9 changes: 6 additions & 3 deletions src-chatbot/public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@
"name": "Personal Chat",
"short_name": "PersonalOS",
"description": "Personal Chat Assistant",
"start_url": "/",
"start_url": "/wp-admin/admin.php?page=personalos-chatbot",
"id": "/wp-admin/admin.php?page=personalos-chatbot",
"display": "standalone",
"display_override": ["standalone", "minimal-ui"],
"orientation": "portrait",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{
"src": "/images/apple-icon.png",
"src": "/wp-content/plugins/personalos/build/chatbot/images/apple-icon.png",
"sizes": "180x180",
"type": "image/png",
"purpose": "any maskable"
}
]
}
}
11 changes: 11 additions & 0 deletions src-chatbot/public/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Service Worker for PersonalOS PWA
// Minimal service worker - just enough to enable PWA installation

self.addEventListener('install', () => {
self.skipWaiting();
});

self.addEventListener('activate', (event) => {
event.waitUntil(clients.claim());
});

2 changes: 2 additions & 0 deletions src/openai/blocks/message/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"attributes": {
"content": {
"type": "string",
"source": "text",
"selector": ".ai-message-text",
"default": ""
},
"role": {
Expand Down
38 changes: 37 additions & 1 deletion src/openai/blocks/message/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -477,4 +477,40 @@
.wp-block-pos-ai-message .markdown-content td {
padding: 6px 8px;
}
}
}

/* Editor textarea styling */
.wp-block-pos-ai-message .message-editor {
width: 100%;
min-height: 100px;
padding: 0;
border: none;
background: transparent;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
font-size: 13px;
line-height: 1.6;
color: inherit;
resize: vertical;
outline: none;
}

.wp-block-pos-ai-message .message-editor::placeholder {
color: inherit;
opacity: 0.5;
}

/* Click-to-edit hint */
.wp-block-pos-ai-message .markdown-content {
cursor: text;
}

.wp-block-pos-ai-message.is-selected .markdown-content:not(:has(textarea))::after {
content: 'Click to edit';
position: absolute;
bottom: 8px;
right: 12px;
font-size: 10px;
opacity: 0.4;
font-style: italic;
pointer-events: none;
}
Loading