Skip to content

Commit

Permalink
feat: Extract page content and send to AI assistant (#126)
Browse files Browse the repository at this point in the history
* Feature: Extract page content and send to AI assistant

* Update UI

* fix: Optimize prompt words

* fix: update iframe src in SidePanelChat component

* feat: Implement standardized AI integration for remaining three panels
  • Loading branch information
HashCookie authored Jun 25, 2024
1 parent 660ba3a commit 7d1181c
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 34 deletions.
41 changes: 34 additions & 7 deletions app/components/SidePanelChat.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,40 @@
import React, { useState } from 'react';
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import { extractPageContent } from '../utils/contentExtractor';

export const SidePanelChat: React.FC = () => {
const SidePanelChat = forwardRef((props, ref) => {
const [isOpen, setIsOpen] = useState(false);
const [message, setMessage] = useState('');
const [pageContent, setPageContent] = useState('');
const [boxType, setBoxType] = useState('');

const toggleDrawer = () => {
setIsOpen(!isOpen);
};

const openDrawer = (message, boxType) => {
setMessage(message);
setBoxType(boxType);
setIsOpen(true);
};

useImperativeHandle(ref, () => {
return {
openDrawer,
};
});

useEffect(() => {
if (isOpen && boxType) {
const { extractedContent } = extractPageContent(boxType);
setPageContent(extractedContent);
}
}, [isOpen, boxType]);

return (
<>
<button className="text-red-600" onClick={toggleDrawer}>
Why this result?
</button>
<div className="text-red-600 flex items-center">
<span>Why this result?</span>
</div>
{isOpen && <div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={toggleDrawer}></div>}
<div
className={`fixed top-0 right-0 w-[500px] h-full bg-white z-50 shadow-lg transform transition-transform duration-300 ease-in-out ${
Expand All @@ -33,7 +56,7 @@ export const SidePanelChat: React.FC = () => {
<iframe
id="iframeHelper"
title="iframeHelper"
src="https://ai.casbin.com/?isRaw=1"
src={`https://ai.casbin.com/?isRaw=1&newMessage=${encodeURIComponent(message)}`}
className="w-full h-full"
scrolling="no"
frameBorder="0"
Expand All @@ -42,4 +65,8 @@ export const SidePanelChat: React.FC = () => {
</div>
</>
);
};
});

SidePanelChat.displayName = 'SidePanelChat';

export default SidePanelChat;
45 changes: 45 additions & 0 deletions app/components/editor/ButtonPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { WidgetType, EditorView, Decoration } from '@codemirror/view';
import { RangeSetBuilder } from '@codemirror/state';

type OpenDrawerCallback = (message: string) => void;

class ButtonWidget extends WidgetType {
openDrawerCallback: OpenDrawerCallback;
extractContentCallback: (boxType: string) => string;
boxType: string;

constructor(openDrawerCallback: OpenDrawerCallback, extractContentCallback: (boxType: string) => string, boxType: string) {
super();
this.openDrawerCallback = openDrawerCallback;
this.extractContentCallback = extractContentCallback;
this.boxType = boxType;
}

toDOM() {
const button = document.createElement('button');
button.className = `flex items-center rounded text-[#453d7d] px-1 border border-[#453d7d]
bg-[#efefef] hover:bg-[#453d7d] hover:text-white transition-colors duration-500
font-medium whitespace-nowrap overflow-hidden`;
button.innerHTML = '<img src="/openai.svg" alt="" class="w-4 h-4 mr-1" /> Ask AI';
button.style.position = 'absolute';
button.style.right = '1px';
button.style.top = '1px';

button.addEventListener('click', () => {
if (this.openDrawerCallback) {
const extractedContent = this.extractContentCallback(this.boxType);
this.openDrawerCallback(extractedContent);
}
});

return button;
}
}

export function buttonPlugin(openDrawerCallback: OpenDrawerCallback, extractContentCallback: (boxType: string) => string, boxType: string) {
return EditorView.decorations.compute([], (state) => {
const builder = new RangeSetBuilder<Decoration>();
builder.add(0, 0, Decoration.widget({ widget: new ButtonWidget(openDrawerCallback, extractContentCallback, boxType) }));
return builder.finish();
});
}
75 changes: 48 additions & 27 deletions app/components/editor/index.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,5 @@
// Copyright 2024 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use client';
import React, { isValidElement, useState, useEffect } from 'react';
import React, { isValidElement, useState, useEffect, useRef } from 'react';
import { example, ModelKind } from './casbin-mode/example';
import { e, m, p, r } from '@/app/components/editor/hooks/useSetupEnforceContext';
import { clsx } from 'clsx';
Expand All @@ -32,7 +18,9 @@ import useShareInfo from '@/app/components/editor/hooks/useShareInfo';
import useCopy from '@/app/components/editor/hooks/useCopy';
import useSetupEnforceContext from '@/app/components/editor/hooks/useSetupEnforceContext';
import useIndex from '@/app/components/editor/hooks/useIndex';
import { SidePanelChat } from '@/app/components/SidePanelChat';
import SidePanelChat from '@/app/components/SidePanelChat';
import { extractPageContent } from '../../utils/contentExtractor';
import { buttonPlugin } from './ButtonPlugin';

export const EditorScreen = () => {
const {
Expand All @@ -50,6 +38,16 @@ export const EditorScreen = () => {
data: enforceContextData,
});
const [casbinVersion, setCasbinVersion] = useState('');
const sidePanelChatRef = useRef<{ openDrawer: (message: string) => void } | null>(null);
const openDrawerWithMessage = (message: string) => {
if (sidePanelChatRef.current) {
sidePanelChatRef.current.openDrawer(message);
}
};
const extractContent = (boxType: string) => {
const { message } = extractPageContent(boxType);
return message;
};

useEffect(() => {
const fetchCasbinVersion = async () => {
Expand All @@ -75,9 +73,7 @@ export const EditorScreen = () => {
} else if (Array.isArray(v)) {
const formattedResults = v.map((res) => {
if (typeof res === 'object') {
const reasonString = Array.isArray(res.reason) && res.reason.length > 0
? ` Reason: ${JSON.stringify(res.reason)}`
: '';
const reasonString = Array.isArray(res.reason) && res.reason.length > 0 ? ` Reason: ${JSON.stringify(res.reason)}` : '';
return `${res.okEx}${reasonString}`;
}
return res;
Expand Down Expand Up @@ -194,7 +190,13 @@ export const EditorScreen = () => {
bracketMatching: true,
indentOnInput: true,
}}
extensions={[basicSetup, CasbinConfSupport(), indentUnit.of(' '), EditorView.lineWrapping]}
extensions={[
basicSetup,
CasbinConfSupport(),
indentUnit.of(' '),
EditorView.lineWrapping,
buttonPlugin(openDrawerWithMessage, extractContent, 'model'),
]}
className={'function flex-grow'}
value={modelText}
/>
Expand All @@ -214,7 +216,13 @@ export const EditorScreen = () => {
<div className="flex flex-col h-full">
<CodeMirror
height="100%"
extensions={[basicSetup, CasbinPolicySupport(), indentUnit.of(' '), EditorView.lineWrapping]}
extensions={[
basicSetup,
CasbinPolicySupport(),
indentUnit.of(' '),
EditorView.lineWrapping,
buttonPlugin(openDrawerWithMessage, extractContent, 'policy'),
]}
basicSetup={{
lineNumbers: true,
highlightActiveLine: true,
Expand Down Expand Up @@ -277,7 +285,13 @@ export const EditorScreen = () => {
onChange={(value) => {
setRequestPersistent(value);
}}
extensions={[basicSetup, CasbinPolicySupport(), indentUnit.of(' '), EditorView.lineWrapping]}
extensions={[
basicSetup,
CasbinPolicySupport(),
indentUnit.of(' '),
EditorView.lineWrapping,
buttonPlugin(openDrawerWithMessage, extractContent, 'request'),
]}
basicSetup={{
lineNumbers: true,
highlightActiveLine: true,
Expand All @@ -293,7 +307,9 @@ export const EditorScreen = () => {
<div className="flex-1 flex flex-col h-full overflow-hidden">
<div className={clsx('h-10 font-bold', 'flex items-center justify-between')}>
<div>Enforcement Result</div>
<div className='mr-4'><SidePanelChat /></div>
<div className="mr-4">
<SidePanelChat ref={sidePanelChatRef} />
</div>
</div>
<div className="flex-grow overflow-auto h-full">
<div className="flex flex-col h-full">
Expand All @@ -303,7 +319,14 @@ export const EditorScreen = () => {
return;
}}
theme={monokai}
extensions={[basicSetup, javascriptLanguage, indentUnit.of(' '), EditorView.lineWrapping, EditorView.editable.of(false)]}
extensions={[
basicSetup,
javascriptLanguage,
indentUnit.of(' '),
EditorView.lineWrapping,
EditorView.editable.of(false),
buttonPlugin(openDrawerWithMessage, extractContent, 'enforcementResult'),
]}
basicSetup={{
lineNumbers: true,
highlightActiveLine: true,
Expand Down Expand Up @@ -365,9 +388,7 @@ export const EditorScreen = () => {
} else if (Array.isArray(v)) {
const formattedResults = v.map((res) => {
if (typeof res === 'object') {
const reasonString = Array.isArray(res.reason) && res.reason.length > 0
? ` Reason: ${JSON.stringify(res.reason)}`
: '';
const reasonString = Array.isArray(res.reason) && res.reason.length > 0 ? ` Reason: ${JSON.stringify(res.reason)}` : '';
return `${res.okEx}${reasonString}`;
}
return res;
Expand Down
56 changes: 56 additions & 0 deletions app/utils/contentExtractor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
const cleanContent = (content: string) => {
return content.replace(/^\d+\s+/gm, '').trim();
};

export const extractPageContent = (boxType: string) => {
const mainContent = document.querySelector('main')?.innerText || 'No main content found';

const customConfigMatch = mainContent.match(/Custom config\s+([\s\S]*?)\s+Model/);
const modelMatch = mainContent.match(/Model\s+([\s\S]*?)\s+Policy/);
const policyMatch = mainContent.match(/Policy\s+([\s\S]*?)\s+Request/);
const requestMatch = mainContent.match(/Request\s+([\s\S]*?)\s+Enforcement Result/);
const enforcementResultMatch = mainContent.match(/Enforcement Result\s+([\s\S]*?)\s+SYNTAX VALIDATE/);

const customConfig = customConfigMatch ? cleanContent(customConfigMatch[1]) : 'No custom config found';
const model = modelMatch ? cleanContent(modelMatch[1].replace(/Select your model[\s\S]*?RESET/, '')) : 'No model found';
const policy = policyMatch ? cleanContent(policyMatch[1].replace(/Node-Casbin v[\d.]+/, '')) : 'No policy found';
const request = requestMatch ? cleanContent(requestMatch[1]) : 'No request found';
const enforcementResult = enforcementResultMatch
? cleanContent(enforcementResultMatch[1].replace(/Why this result\?[\s\S]*?AI Assistant/, ''))
: 'No enforcement result found';

const extractedContent = `
Custom Config: ${customConfig}
Model: ${model}
Policy: ${policy}
Request: ${request}
Enforcement Result: ${enforcementResult}
`;

let message = '';
switch (boxType) {
case 'model':
message = `Briefly explain the Model content.
no need to repeat the content of the question.\n${extractedContent}`;
break;
case 'policy':
message = `Briefly explain the Policy content.
no need to repeat the content of the question.\n${extractedContent}`;
break;
case 'request':
message = `Briefly explain the Request content.
no need to repeat the content of the question.\n${extractedContent}`;
break;
case 'enforcementResult':
message = `Why this result? please provide a brief summary.
no need to repeat the content of the question.\n${extractedContent}`;
break;
default:
message = extractedContent;
}

return {
extractedContent,
message,
};
};
1 change: 1 addition & 0 deletions public/openai.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 7d1181c

Please sign in to comment.